diff --git a/plugins/spellchecker/lib/patricia-trie-0.1.jar b/plugins/spellchecker/lib/patricia-trie-0.1.jar new file mode 100644 index 000000000000..5d1ea6b242f0 Binary files /dev/null and b/plugins/spellchecker/lib/patricia-trie-0.1.jar differ diff --git a/plugins/spellchecker/spellchecker.iml b/plugins/spellchecker/spellchecker.iml index 80754d1ebb18..517fa99442d0 100644 --- a/plugins/spellchecker/spellchecker.iml +++ b/plugins/spellchecker/spellchecker.iml @@ -9,17 +9,6 @@ - - - - - - - - - - - diff --git a/plugins/spellchecker/src/META-INF/plugin.xml b/plugins/spellchecker/src/META-INF/plugin.xml index be891f33b0cb..59b8f91cdae5 100644 --- a/plugins/spellchecker/src/META-INF/plugin.xml +++ b/plugins/spellchecker/src/META-INF/plugin.xml @@ -9,18 +9,23 @@ - com.intellij.spellchecker.options.CachedDictionaryState - com.intellij.spellchecker.options.CachedDictionaryState + com.intellij.spellchecker.state.CachedDictionaryState + com.intellij.spellchecker.state.CachedDictionaryState - com.intellij.spellchecker.options.ProjectDictionaryState - com.intellij.spellchecker.options.ProjectDictionaryState + com.intellij.spellchecker.state.ProjectDictionaryState + com.intellij.spellchecker.state.ProjectDictionaryState + + + com.intellij.spellchecker.state.AggregatedDictionaryState + com.intellij.spellchecker.state.AggregatedDictionaryState + @@ -51,7 +56,7 @@ - + diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/FileLoader.java b/plugins/spellchecker/src/com/intellij/spellchecker/FileLoader.java new file mode 100644 index 000000000000..f22aec71f1b1 --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/FileLoader.java @@ -0,0 +1,56 @@ +/* + * Copyright 2000-2009 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.intellij.spellchecker; + +import com.intellij.spellchecker.dictionary.Loader; +import com.intellij.spellchecker.dictionary.Processor; +import org.jetbrains.annotations.NotNull; + +import java.io.*; + + +public class FileLoader implements Loader { + + private String url; + + public FileLoader(String url) { + this.url = url; + } + + public void load(@NotNull Processor processor) { + + InputStream io = SpellCheckerManager.class.getResourceAsStream(url); + DataInputStream in = new DataInputStream(io); + BufferedReader br = new BufferedReader(new InputStreamReader(in)); + try{ + String strLine; + while ((strLine = br.readLine()) != null) { + processor.process(strLine); + } + in.close(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + br.close(); + } + catch (IOException ignored) { + + } + } + } + +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/SpellCheckerManager.java b/plugins/spellchecker/src/com/intellij/spellchecker/SpellCheckerManager.java index 5ae1687331ae..304701ae606f 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/SpellCheckerManager.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/SpellCheckerManager.java @@ -1,257 +1,148 @@ -/* - * Copyright 2000-2009 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.intellij.spellchecker; - -import com.intellij.codeHighlighting.HighlightDisplayLevel; -import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.components.ServiceManager; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.project.ProjectManager; -import com.intellij.openapi.diagnostic.Logger; -import com.intellij.spellchecker.engine.SpellChecker; -import com.intellij.spellchecker.engine.SpellCheckerFactory; -import com.intellij.spellchecker.options.SpellCheckerConfiguration; -import com.intellij.spellchecker.options.ProjectDictionaryState; -import com.intellij.spellchecker.options.CachedDictionaryState; -import com.intellij.spellchecker.util.Strings; -import com.intellij.spellchecker.dictionary.Dictionary; -import org.jetbrains.annotations.NonNls; -import org.jetbrains.annotations.NotNull; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; -import java.util.*; - -/** - * Spell checker inspection provider. - */ -public final class SpellCheckerManager { - private static final Logger LOG = Logger.getInstance("#com.intellij.spellchecker.SpellCheckerManager"); - private static final int MAX_SUGGESTIONS_THRESHOLD = 10; - private Project project; - private static HighlightDisplayLevel level; - private Set dictionaries = new HashSet(); - - private Dictionary projectWordList; - private Dictionary cachedWordList; - - public Set getDictionaries() { - return dictionaries; - } - - @NonNls - private static final String[] DICT_URLS = new String[]{"english.dic", "jetbrains.dic"}; - - - public static SpellCheckerManager getInstance(Project project) { - return ServiceManager.getService(project, SpellCheckerManager.class); - } - - private final SpellCheckerConfiguration configuration; - private final SpellChecker spellChecker = SpellCheckerFactory.create(); - - - public SpellCheckerManager(SpellCheckerConfiguration configuration, final Project project) { - this.configuration = configuration; - this.project = project; - reloadConfiguration(); - } - - @NotNull - public static HighlightDisplayLevel getHighlightDisplayLevel() { - return HighlightDisplayLevel.find(SpellCheckerSeveritiesProvider.TYPO); - } - - - @NotNull - public SpellChecker getSpellChecker() { - return spellChecker; - } - - - public boolean hasProblem(@NotNull String word) { - return !isIgnored(word) && !spellChecker.isCorrect(word); - } - - private boolean isIgnored(@NotNull String word) { - return spellChecker.isIgnored(word.toLowerCase()); - } - - public List getVariants(@NotNull String prefix) { - return spellChecker.getVariants(prefix); - } - - @NotNull - public List getSuggestions(@NotNull String word) { - if (!isIgnored(word) && !spellChecker.isCorrect(word)) { - List suggestions = spellChecker.getSuggestions(word, MAX_SUGGESTIONS_THRESHOLD); - if (suggestions.size() != 0) { - boolean capitalized = Strings.isCapitalized(word); - boolean upperCases = Strings.isUpperCase(word); - - if (capitalized) { - Strings.capitalize(suggestions); - } - else if (upperCases) { - Strings.upperCase(suggestions); - } - } - List result = new ArrayList(); - for (String s : suggestions) { - if (!result.contains(s)) { - result.add(s); - } - } - return result; - } - return Collections.emptyList(); - } - - @NotNull - public List getSuggestionsExt(@NotNull String word) { - return spellChecker.getSuggestionsExt(word, MAX_SUGGESTIONS_THRESHOLD); - } - - - /** - * Load dictionary from stream. - * - * @param inputStream Dictionary input stream - * @throws java.io.IOException if dictionary load with problems - */ - public void addDictionary(@NotNull InputStream inputStream) throws IOException { - addDictionary(inputStream, Charset.defaultCharset().name()); - } - - /** - * Load dictionary from stream. - * - * @param inputStream Dictionary input stream - * @param encoding Encoding - * @throws java.io.IOException if dictionary load with problems - */ - public void addDictionary(@NotNull InputStream inputStream, @NonNls String encoding) throws IOException { - addDictionary(inputStream, encoding, Locale.getDefault()); - } - - /** - * Load dictionary from stream. - * - * @param inputStream Dictionary input stream - * @param encoding Encoding - * @param locale Locale of dictionary - * @throws java.io.IOException if dictionary load with problems - */ - public void addDictionary(@NotNull InputStream inputStream, @NonNls String encoding, @NonNls @NotNull Locale locale) throws IOException { - spellChecker.addDictionary(inputStream, encoding, locale); - } - - public void acceptWordAsCorrect(@NotNull String word) { - String lowerCased = word.toLowerCase(); - projectWordList.acceptWord(word); - cachedWordList.acceptWord(word); - spellChecker.addToDictionary(lowerCased); - } - - public void reloadConfiguration() { - initDictionaries(); - spellChecker.reset(); - - projectWordList = ServiceManager.getService(project, ProjectDictionaryState.class).getDictionary(); - cachedWordList = ServiceManager.getService(project, CachedDictionaryState.class).getDictionary(); - - for (String word : projectWordList.getWords()) { - cachedWordList.acceptWord(word); - } - - - for (String word : ejectAll(cachedWordList.getWords())) { - String lowerCased = word.toLowerCase(); - spellChecker.addToDictionary(lowerCased); - } - - } - - public void applyConfiguration() { - initDictionaries(); - spellChecker.reset(); - - assert projectWordList != null; - assert cachedWordList != null; - - for (String word : projectWordList.getWords()) { - cachedWordList.acceptWord(word); - } - - - for (String word : ejectAll(cachedWordList.getWords())) { - String lowerCased = word.toLowerCase(); - spellChecker.addToDictionary(lowerCased); - } - - } - - private void initDictionaries() { - for (String dictUrl : DICT_URLS) { - initDictionary(dictUrl); - } - } - - private void initDictionary(String url) { - InputStream is = SpellCheckerManager.class.getResourceAsStream(url); - if (is != null) { - try { - dictionaries.add(url); - addDictionary(is); - } - catch (IOException e) { - LOG.error("Dictionary could not be loaded", e); - } - } - } - - public void restartInspections() { - //reloadConfiguration(); - ApplicationManager.getApplication().invokeLater(new Runnable() { - public void run() { - Project[] projects = ProjectManager.getInstance().getOpenProjects(); - for (Project project : projects) { - if (project.isInitialized() && project.isOpen() && !project.isDefault()) { - DaemonCodeAnalyzer.getInstance(project).restart(); - } - } - } - }); - } - - private HashSet ejectAll(Set from) { - HashSet words = new HashSet(from); - // from.clear(); - return words; - } - - public Dictionary getProjectWordList() { - return projectWordList; - } - - public Dictionary getCachedWordList() { - return cachedWordList; - } - - -} +/* + * Copyright 2000-2009 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.intellij.spellchecker; + +import com.intellij.codeHighlighting.HighlightDisplayLevel; +import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ProjectManager; +import com.intellij.spellchecker.dictionary.Dictionary; +import com.intellij.spellchecker.dictionary.Loader; +import com.intellij.spellchecker.engine.SpellCheckerEngine; +import com.intellij.spellchecker.engine.SpellCheckerFactory; +import com.intellij.spellchecker.state.StateLoader; +import com.intellij.spellchecker.util.Strings; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +public class SpellCheckerManager { + + private static final int MAX_SUGGESTIONS_THRESHOLD = 5; + private static final int MAX_METRICS = 1; + + private Project project; + + private SpellCheckerEngine spellChecker; + + private Dictionary userDictionary; + + public static SpellCheckerManager getInstance(Project project) { + return ServiceManager.getService(project, SpellCheckerManager.class); + } + + public SpellCheckerManager(Project project) { + this.project = project; + reloadConfiguration(); + } + + public Dictionary getUserDictionary() { + return userDictionary; + } + + public void reloadConfiguration() { + spellChecker = SpellCheckerFactory.create(); + fillEngineDictionary(); + } + + private void fillEngineDictionary() { + spellChecker.reset(); + final StateLoader stateLoader = new StateLoader(project); + Loader[] loaders = new Loader[]{new FileLoader("english.dic"), new FileLoader("jetbrains.dic"), stateLoader}; + for (Loader loader : loaders) { + spellChecker.loadDictionary(loader); + } + userDictionary = stateLoader.getDictionary(); + } + + + public boolean hasProblem(@NotNull String word) { + return !spellChecker.isCorrect(word); + } + + public void acceptWordAsCorrect(@NotNull String word) { + final String transformed = spellChecker.getTransformation().transform(word); + if (transformed != null) { + userDictionary.addToDictionary(transformed); + spellChecker.addToDictionary(transformed); + } + } + + public void updateUserWords(@Nullable Collection words) { + Set transformed = spellChecker.getTransformation().transform(words); + userDictionary.replaceAll(transformed); + fillEngineDictionary(); + restartInspections(); + } + + + @NotNull + public static HighlightDisplayLevel getHighlightDisplayLevel() { + return HighlightDisplayLevel.find(SpellCheckerSeveritiesProvider.TYPO); + } + + + @NotNull + public List getSuggestions(@NotNull String word) { + if (!spellChecker.isCorrect(word)) { + List suggestions = spellChecker.getSuggestions(word, MAX_SUGGESTIONS_THRESHOLD, MAX_METRICS); + if (suggestions.size() != 0) { + boolean capitalized = Strings.isCapitalized(word); + boolean upperCases = Strings.isUpperCase(word); + if (capitalized) { + Strings.capitalize(suggestions); + } + else if (upperCases) { + Strings.upperCase(suggestions); + } + } + List result = new ArrayList(); + for (String s : suggestions) { + if (!result.contains(s)) { + result.add(s); + } + } + return result; + } + return Collections.emptyList(); + } + + @NotNull + public List getVariants(@NotNull String prefix) { + + return Collections.emptyList(); + } + + + public void restartInspections() { + ApplicationManager.getApplication().invokeLater(new Runnable() { + public void run() { + Project[] projects = ProjectManager.getInstance().getOpenProjects(); + for (Project project : projects) { + if (project.isInitialized() && project.isOpen() && !project.isDefault()) { + DaemonCodeAnalyzer.getInstance(project).restart(); + } + } + } + }); + } + + + +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/AggregatedDictionary.java b/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/AggregatedDictionary.java new file mode 100644 index 000000000000..a5297adc0d37 --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/AggregatedDictionary.java @@ -0,0 +1,120 @@ +/* + * Copyright 2000-2009 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.intellij.spellchecker.dictionary; + +import com.intellij.spellchecker.trie.Action; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +public class AggregatedDictionary implements Dictionary { + + private static final String DICTIONARY_NAME = "common"; + private Dictionary cachedDictionary; + private ProjectDictionary projectDictionary; + + public String getName() { + return DICTIONARY_NAME; + } + + public AggregatedDictionary(@NotNull ProjectDictionary projectDictionary, @NotNull Dictionary cachedDictionary) { + this.projectDictionary = projectDictionary; + this.cachedDictionary = cachedDictionary; + this.cachedDictionary.addToDictionary(projectDictionary.getWords()); + } + + public boolean isEmpty() { + return false; + } + + public boolean contains(String word) { + if (word == null) { + return false; + } + return cachedDictionary.contains(word); + } + + public void addToDictionary(String word) { + getProjectDictionary().addToDictionary(word); + getCachedDictionary().addToDictionary(word); + } + + public void removeFromDictionary(String word) { + getProjectDictionary().removeFromDictionary(word); + getCachedDictionary().removeFromDictionary(word); + } + + + public void replaceAll(@Nullable Collection words) { + Set oldWords = getProjectDictionary().getWords(); + getProjectDictionary().replaceAll(words); + if (oldWords != null) { + for (String word : oldWords) { + if (words == null || !words.contains(word)) { + getCachedDictionary().removeFromDictionary(word); + } + } + } + } + + public void clear() { + getProjectDictionary().clear(); + } + + public void traverse(final Action action) { + cachedDictionary.traverse(action); + } + + + public Set getWords() { + Set words = new HashSet(); + words.addAll(cachedDictionary.getWords()); + return words; + } + + @Nullable + public Set getEditableWords() { + return getProjectDictionary().getEditableWords(); + } + + @Nullable + public Set getNotEditableWords() { + Set words = getWords(); + Set editable = getEditableWords(); + if (words != null && editable != null) { + words.removeAll(editable); + } + return words; + } + + public void addToDictionary(@Nullable Collection words) { + getProjectDictionary().addToDictionary(words); + getCachedDictionary().addToDictionary(words); + } + + public Dictionary getCachedDictionary() { + return cachedDictionary; + } + + public ProjectDictionary getProjectDictionary() { + return projectDictionary; + } + + +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/Dictionary.java b/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/Dictionary.java index c961b5f7c5c2..d5b51f37d5ce 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/Dictionary.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/Dictionary.java @@ -1,32 +1,54 @@ -/* - * Copyright 2000-2009 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.intellij.spellchecker.dictionary; - -import org.jetbrains.annotations.NotNull; - -import java.util.Set; - - -public interface Dictionary { - - Set getWords(); - - void acceptWord(@NotNull String word); - - void replaceAllWords(Set newWords); - - String getName(); -} +/* + * Copyright 2000-2009 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.intellij.spellchecker.dictionary; + +import com.intellij.spellchecker.trie.Action; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.Set; + +public interface Dictionary { + + String getName(); + + boolean contains(String word); + + boolean isEmpty(); + + void addToDictionary(String word); + + void removeFromDictionary(String word); + + void addToDictionary(@Nullable Collection words); + + void replaceAll(@Nullable Collection words); + + void clear(); + + void traverse(final Action action); + + @Nullable + Set getWords(); + + @Nullable + Set getEditableWords(); + + @Nullable + Set getNotEditableWords(); + + +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/Loader.java b/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/Loader.java new file mode 100644 index 000000000000..6a24dc79b2df --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/Loader.java @@ -0,0 +1,24 @@ +/* + * Copyright 2000-2009 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.intellij.spellchecker.dictionary; + +import org.jetbrains.annotations.NotNull; + + +public interface Loader { + + void load(@NotNull Processor processor); +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/Processor.java b/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/Processor.java new file mode 100644 index 000000000000..22b6fab395c9 --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/Processor.java @@ -0,0 +1,23 @@ +/* + * Copyright 2000-2009 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.intellij.spellchecker.dictionary; + +import org.jetbrains.annotations.Nullable; + +public interface Processor { + + void process(@Nullable String word); +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/ProjectDictionary.java b/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/ProjectDictionary.java index 1844f204576f..04b4156d3c9f 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/ProjectDictionary.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/ProjectDictionary.java @@ -1,89 +1,192 @@ -/* - * Copyright 2000-2009 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.intellij.spellchecker.dictionary; - -import com.intellij.util.containers.HashSet; -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.List; -import java.util.Set; - -/** - * Created by IntelliJ IDEA. - * - * @author shkate@jetbrains.com - */ -public class ProjectDictionary extends UserDictionary { - - @NotNull - public List dictionaries = new ArrayList(); - - public ProjectDictionary() { - } - - public ProjectDictionary(String name) { - super(name); - } - - @Override - public Set getWords() { - Set words = new HashSet(); - for (Dictionary dictionary : dictionaries) { - words.addAll(dictionary.getWords()); - } - return words; - } - - @Override - public void acceptWord(@NotNull String word) { - getUserDictionary().acceptWord(word); - } - - @Override - public void replaceAllWords(Set newWords) { - getUserDictionary().replaceAllWords(newWords); - } - - public void setDictionaries(@NotNull List dictionaries) { - this.dictionaries = dictionaries; - } - - @NotNull - public List getDictionaries(){ - return dictionaries; - } - - public Dictionary getUserDictionary() { - final String name = getCurrentUserName(); - Dictionary userDictionary = null; - for (Dictionary dictionary : dictionaries) { - if (dictionary.getName().equals(name)){ - userDictionary = dictionary; - break; - } - } - if (userDictionary==null){ - userDictionary = new UserDictionary(name); - dictionaries.add((UserDictionary)userDictionary); - } - return userDictionary; - } - - public static String getCurrentUserName() { - return System.getProperty("user.name"); - } -} +/* + * Copyright 2000-2009 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.intellij.spellchecker.dictionary; + +import com.intellij.spellchecker.trie.Action; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +public class ProjectDictionary implements Dictionary { + + private static final String DEFAULT_CURRENT_USER_NAME = "default.user"; + private static final String DEFAULT_PROJECT_DICTIONARY_NAME = "project"; + private String activeName; + private Set dictionaries; + + + public ProjectDictionary() { + } + + public ProjectDictionary(Set dictionaries) { + this.dictionaries = dictionaries; + } + + public boolean isEmpty() { + return false; + } + + public String getName() { + return DEFAULT_PROJECT_DICTIONARY_NAME; + } + + public String getActiveName() { + return activeName; + } + + public void setActiveName(String name) { + this.activeName = name; + } + + public boolean contains(String word) { + if (word == null || dictionaries == null) { + return false; + } + for (Dictionary dictionary : dictionaries) { + if (dictionary.contains(word)) { + return true; + } + } + return false; + } + + public void addToDictionary(String word) { + getActiveDictionary().addToDictionary(word); + } + + public void removeFromDictionary(String word) { + getActiveDictionary().removeFromDictionary(word); + } + + @NotNull + private Dictionary getActiveDictionary() { + return ensureCurrentUserDictionary(); + } + + @NotNull + private Dictionary ensureCurrentUserDictionary() { + if (activeName == null) { + activeName = DEFAULT_CURRENT_USER_NAME; + } + Dictionary result = getDictionaryByName(activeName); + if (result == null) { + result = new UserDictionary(this.activeName); + if (dictionaries == null) { + dictionaries = new HashSet(); + } + dictionaries.add(result); + } + return result; + } + + @Nullable + private Dictionary getDictionaryByName(@NotNull String name) { + if (dictionaries == null) { + return null; + } + Dictionary result = null; + for (Dictionary dictionary : dictionaries) { + if (dictionary.getName().equals(name)) { + result = dictionary; + break; + } + } + return result; + } + + + public void replaceAll(@Nullable Collection words) { + getActiveDictionary().replaceAll(words); + } + + public void clear() { + getActiveDictionary().clear(); + } + + + @Nullable + public Set getWords() { + if (dictionaries == null) { + return null; + } + Set words = new HashSet(); + for (Dictionary dictionary : dictionaries) { + words.addAll(dictionary.getWords()); + } + return words; + } + + public void traverse(Action action) { + if (dictionaries == null) { + return; + } + + for (Dictionary dictionary : dictionaries) { + dictionary.traverse(action); + } + + } + + @Nullable + public Set getEditableWords() { + return getActiveDictionary().getWords(); + } + + @Nullable + public Set getNotEditableWords() { + Set words = getWords(); + Set editable = getEditableWords(); + if (words != null && editable != null) { + words.removeAll(editable); + } + return words; + } + + public void addToDictionary(@Nullable Collection words) { + getActiveDictionary().addToDictionary(words); + } + + public Set getDictionaries() { + return dictionaries; + } + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ProjectDictionary that = (ProjectDictionary)o; + + if (activeName != null ? !activeName.equals(that.activeName) : that.activeName != null) return false; + if (dictionaries != null ? !dictionaries.equals(that.dictionaries) : that.dictionaries != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = activeName != null ? activeName.hashCode() : 0; + result = 31 * result + (dictionaries != null ? dictionaries.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "ProjectDictionary{" + "activeName='" + activeName + '\'' + ", dictionaries=" + dictionaries + '}'; + } +} \ No newline at end of file diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/UserDictionary.java b/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/UserDictionary.java index 7c5db10f6b9a..da8f576c5395 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/UserDictionary.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/UserDictionary.java @@ -1,73 +1,126 @@ -/* - * Copyright 2000-2009 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.intellij.spellchecker.dictionary; - -import com.intellij.util.xmlb.annotations.AbstractCollection; -import com.intellij.util.xmlb.annotations.Attribute; -import com.intellij.util.xmlb.annotations.Tag; -import org.jetbrains.annotations.NotNull; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -/** - * Created by IntelliJ IDEA. - * - * @author shkate@jetbrains.com - */ -@Tag("dictionary") -public class UserDictionary implements Dictionary { - - @Tag("words") - @AbstractCollection(surroundWithTag = false,elementTag = "w",elementValueAttribute = "") - public Set words = new HashSet(); - - @Attribute(NAME_ATTRIBUTE) - public String name = "new"; - private static final String NAME_ATTRIBUTE = "name"; - - public UserDictionary() { - } - - public UserDictionary(String name) { - this.name = name; - } - - @NotNull - public String getName() { - return name; - } - - - public Set getWords() { - return Collections.unmodifiableSet(words); - } - - public void acceptWord(@NotNull String word) { - words.add(word); - } - - public void replaceAllWords(Set newWords) { - replaceAll(words, newWords); - } - - private static void replaceAll(Set words, Set newWords) { - words.clear(); - words.addAll(newWords); - } - -} +/* + * Copyright 2000-2009 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.intellij.spellchecker.dictionary; + +import com.intellij.spellchecker.trie.Action; +import com.intellij.spellchecker.trie.CharSequenceKeyAnalyzer; +import com.intellij.spellchecker.trie.PatriciaTrie; +import com.intellij.spellchecker.trie.Trie; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +public class UserDictionary implements Dictionary { + + private String name; + private Trie trie = new PatriciaTrie(new CharSequenceKeyAnalyzer()); + + public UserDictionary(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public boolean contains(String word) { + return word != null && trie.containsKey(word); + } + + @Nullable + public Set getWords() { + return trie.keySet(); + } + + @Nullable + public Set getEditableWords() { + return trie.keySet(); + } + + @Nullable + public Set getNotEditableWords() { + return null; + } + + public void clear() { + trie.clear(); + } + + + public void addToDictionary(String word) { + if (word == null) { + return; + } + trie.put(word, word); + } + + public void removeFromDictionary(String word) { + if (word == null) { + return; + } + trie.remove(word); + } + + public void replaceAll(@Nullable Collection words) { + clear(); + addToDictionary(words); + } + + public void addToDictionary(@Nullable Collection words) { + if (words == null || words.isEmpty()) { + return; + } + for (String word : words) { + addToDictionary(word); + } + } + + public boolean isEmpty() { + return (trie == null || trie.size() == 0); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + UserDictionary that = (UserDictionary)o; + + return !(name != null ? !name.equals(that.name) : that.name != null); + + } + + public void traverse(final Action action){ + trie.traverse(new PatriciaTrie.Cursor() { + public SelectStatus select(Map.Entry entry) { + action.run(entry); + return SelectStatus.CONTINUE; + } + }); + } + + @Override + public int hashCode() { + return name != null ? name.hashCode() : 0; + } + + @Override + public String toString() { + return "UserDictionary{" + "name='" + name + '\'' + ", words.count=" + trie.size() + '}'; + } +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/engine/BaseSpellChecker.java b/plugins/spellchecker/src/com/intellij/spellchecker/engine/BaseSpellChecker.java new file mode 100644 index 000000000000..ef5532fe1ea6 --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/engine/BaseSpellChecker.java @@ -0,0 +1,115 @@ +/* + * Copyright 2000-2009 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.intellij.spellchecker.engine; + +import com.intellij.spellchecker.dictionary.*; +import com.intellij.spellchecker.trie.Action; +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; + + +public class BaseSpellChecker implements SpellCheckerEngine { + + private Dictionary engineDictionary; + + private Transformation transform = new Transformation(); + + private Metrics metrics = new LevenshteinDistance(); + + private Processor processor = new Processor() { + public void process(@Nullable String word) { + final String transformed = transform.transform(word); + if (transformed != null) { + engineDictionary.addToDictionary(transformed); + } + } + }; + + public BaseSpellChecker() { + ensureEngineDictionary(); + } + + public void loadDictionary(@NotNull Loader loader) { + loader.load(processor); + } + + public Transformation getTransformation() { + return transform; + } + + private void ensureEngineDictionary() { + if (engineDictionary == null) { + engineDictionary = new UserDictionary("engine"); + } + } + + + public void addToDictionary(String word) { + final String transformed = transform.transform(word); + if (transformed != null) { + engineDictionary.addToDictionary(transformed); + } + } + + public boolean isCorrect(@NotNull String word) { + final String transformed = transform.transform(word); + return transformed != null && engineDictionary.contains(transformed); + } + + @NotNull + public List getSuggestions(final @NotNull String word, int threshold, int quality) { + final List suggestions = new ArrayList(); + + suggestions.clear(); + engineDictionary.traverse(new Action() { + public void run(Map.Entry entry) { + if (word.charAt(0) == entry.getKey().charAt(0)/* && Math.abs(mpw.length() - entry.getKey().length()) <= 1*/) { + final int distance = metrics.calculateMetrics(word, entry.getKey()); + suggestions.add(new Suggestion(entry.getKey(), distance)); + } + } + }); + List result = new ArrayList(); + if (suggestions.isEmpty()){ + return result; + } + Collections.sort(suggestions); + int bestMetrics = suggestions.get(0).getMetrics(); + for (int i = 0; i < threshold; i++) { + + if (suggestions.size()quality){ + break; + } + result.add(i,suggestions.get(i).getWord()); + } + return result; + } + + @NotNull + public List getVariants(@NotNull String prefix) { + return null; + } + + public void reset() { + engineDictionary = null; + ensureEngineDictionary(); + } +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/engine/JazzySpellChecker.java b/plugins/spellchecker/src/com/intellij/spellchecker/engine/JazzySpellChecker.java deleted file mode 100644 index a5ee8e24fecd..000000000000 --- a/plugins/spellchecker/src/com/intellij/spellchecker/engine/JazzySpellChecker.java +++ /dev/null @@ -1,286 +0,0 @@ -/* - * Copyright 2000-2009 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.intellij.spellchecker.engine; - -import com.swabunga.spell.engine.SpellDictionaryHashMap; -import com.swabunga.spell.engine.Word; -import com.intellij.psi.codeStyle.NameUtil; -import org.jetbrains.annotations.NonNls; -import org.jetbrains.annotations.NotNull; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.util.*; - -/** - * Jazzy implementation of Spell Checker. - */ -final class JazzySpellChecker implements SpellChecker { - private final SpellCheckerWrapper delegate = new SpellCheckerWrapper(); - private final Map> dictionaries = new HashMap>(); - private final Set allowed = new HashSet(); - private SpellDictionaryImpl userDictionary; - - JazzySpellChecker() { - setUserDictionary(); - } - - public void addDictionary(@NotNull InputStream is, @NonNls String encoding, @NotNull Locale locale) throws IOException { - SpellDictionaryImpl spellDictionary = new SpellDictionaryImpl(new InputStreamReader(is, encoding), locale); - Set indexedChars = spellDictionary.getIndexedChars(); - dictionaries.put(spellDictionary, indexedChars); - allowed.addAll(indexedChars); - delegate.addDictionary(spellDictionary); - } - - public void addToDictionary(@NotNull String word) { - delegate.addToDictionary(word); - indexWord(word); - } - - private void indexWord(CharSequence word) { - indexWord(word, allowed); - } - - private static void indexWord(CharSequence word, Set index) { - for (int i = 0; i < word.length(); i++) { - index.add(word.charAt(i)); - } - } - - public void ignoreAll(@NotNull String word) { - delegate.ignoreAll(word); - } - - public boolean isIgnored(@NotNull String word) { - return !isEntireWordAllowed(word, allowed) || delegate.isIgnored(word); - } - - public boolean isCorrect(@NotNull String word) { - return !isEntireWordAllowed(word, allowed) || delegate.isCorrect(word); - } - - private static boolean isEntireWordAllowed(CharSequence word, Set index) { - for (int i = 0; i < word.length(); i++) { - if (!index.contains(word.charAt(i))) { - return false; - } - } - return true; - } - - private Set findDictionaryIndex(String word) { - Set commonIndex = null; - Set lastIndex = null; - Collection> indexes = dictionaries.values(); - for (Set index : indexes) { - if (isEntireWordAllowed(word, index)) { - if (lastIndex != null && commonIndex == null) { - commonIndex = new HashSet(lastIndex.size() + index.size()); - commonIndex.addAll(lastIndex); - lastIndex = commonIndex; - } - if (commonIndex != null) { - commonIndex.addAll(index); - } else { - lastIndex = index; - } - } - } - return lastIndex; - } - - @NotNull - @SuppressWarnings({"unchecked"}) - public List getSuggestions(@NotNull String word, int threshold) { - List words = delegate.getSuggestions(word, threshold); - Set index = findDictionaryIndex(word); - List strings = new ArrayList(words.size()); - for (Word w : words) { - String suggestion = w.getWord(); - if (index == null || isEntireWordAllowed(suggestion, index)) { - strings.add(suggestion); - } - } - return strings; - } - - @NotNull - public List getSuggestionsExt(@NotNull String text, int threshold) { - String[] words = NameUtil.nameToWords(text); - List result = new ArrayList(); - int index = 0; - List[] res = new List[words.length]; - int i = 0; - for (String word : words) { - int start = text.indexOf(word, index); - int end = start + word.length(); - if (!isCorrect(word)) { - List variants = new ArrayList(); - variants.add(word); - res[i++] = variants; - } else { - List variants = getSuggestions(word, threshold); - res[i++] = variants; - } - index = end; - } - - - int counter[] = new int[i]; - int size = 1; - for (int j = 0; j < i; j++) { - size *= res[j].size(); - } - String[] all = new String[size]; - - for (int k = 0; k < size; k++) { - for (int j = 0; j < i; j++) { - if (all[k] == null) { - all[k] = ""; - } - all[k] += res[j].get(counter[j]); - counter[j]++; - if (counter[j] >= res[j].size()) { - counter[j] = 0; - } - } - } - - result.addAll(Arrays.asList(all)); - return result; - } - - @NotNull - public List getVariants(@NotNull String prefix) { - if (prefix.length() > 0) { - List variants = new ArrayList(); - userDictionary.appendWordsStartsWith(prefix, variants); - Set index = new HashSet(); - indexWord(prefix, index); - for (SpellDictionaryImpl dictionary : dictionaries.keySet()) { - Set dictionaryIndex = dictionaries.get(dictionary); - if (isSame(index, dictionaryIndex)) { - dictionary.appendWordsStartsWith(prefix, variants); - } - } - Collections.sort(variants); - return variants; - } - return Collections.emptyList(); - } - - private static boolean isSame(@NotNull Set i1, @NotNull Set i2) { - if (i1.equals(i2)) { - return true; - } - for (Character c : i1) { - if (!i2.contains(c)) { - return false; - } - } - return true; - } - - public void reset() { - delegate.reset(); - allowed.clear(); - Collection> sets = dictionaries.values(); - for (Set set : sets) { - allowed.addAll(set); - } - setUserDictionary(); - } - - private void setUserDictionary() { - try { - userDictionary = new SpellDictionaryImpl(Locale.getDefault()); - delegate.setUserDictionary(userDictionary); - } - catch (IOException e) { - throw new RuntimeException(e); - } - } - - private static class SpellDictionaryImpl extends SpellDictionaryHashMap { - private final Locale locale; - - public SpellDictionaryImpl(Locale locale) throws IOException { - this.locale = locale; - } - - private SpellDictionaryImpl(Reader wordList, Locale locale) throws IOException { - super(wordList); - this.locale = locale; - } - - @SuppressWarnings({"unchecked"}) - public Set getIndexedChars() { - Set index = new HashSet(64); - Collection> values = mainDictionary.values(); - for (List wordList : values) { - if (wordList != null) { - for (String word : wordList) { - if (word != null) { - indexWord(word, index); - } - } - } - } - return Collections.unmodifiableSet(index); - } - - @SuppressWarnings({"unchecked"}) - public void appendWordsStartsWith(@NotNull String prefix, @NotNull Collection buffer) { - String prefixLowerCase = prefix.toLowerCase(locale); - Collection> values = mainDictionary.values(); - int prefixLength = prefix.length(); - StringBuilder builder = new StringBuilder(); - for (List wordList : values) { - if (wordList != null) { - for (String word : wordList) { - if (word != null) { - String lowerCased = word.toLowerCase(locale); - int length = lowerCased.length(); - if (lowerCased.startsWith(prefixLowerCase) && length > prefixLength) { - builder.setLength(0); - builder.append(prefix); - builder.append(lowerCased, prefixLength, length); - String value = builder.toString(); - if (!buffer.contains(value)) { - buffer.add(value); - } - } - } - } - } - } - } - } - - private static final class SpellCheckerWrapper extends com.swabunga.spell.event.SpellChecker { - private SpellCheckerWrapper() { - // Disable caching - setCache(0); - } - - public List getSuggestions(String word, int threshold) { - return super.getSuggestions(word, threshold); - } - } -} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/engine/LevenshteinDistance.java b/plugins/spellchecker/src/com/intellij/spellchecker/engine/LevenshteinDistance.java new file mode 100644 index 000000000000..395e4ab86f9e --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/engine/LevenshteinDistance.java @@ -0,0 +1,43 @@ +/* + * Copyright 2000-2009 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.intellij.spellchecker.engine; + +public class LevenshteinDistance implements Metrics { + + private static int minimum(int a, int b, int c) { + return Math.min(Math.min(a, b), c); + } + + public int calculateMetrics(CharSequence str1, CharSequence str2) { + int[][] distance = new int[str1.length() + 1][str2.length() + 1]; + + for (int i = 0; i <= str1.length(); i++) { + distance[i][0] = i; + } + for (int j = 0; j <= str2.length(); j++) { + distance[0][j] = j; + } + + for (int i = 1; i <= str1.length(); i++) { + for (int j = 1; j <= str2.length(); j++) { + distance[i][j] = minimum(distance[i - 1][j] + 1, distance[i][j - 1] + 1, + distance[i - 1][j - 1] + ((str1.charAt(i - 1) == str2.charAt(j - 1)) ? 0 : 1)); + } + } + + return distance[str1.length()][str2.length()]; + } +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/engine/Metrics.java b/plugins/spellchecker/src/com/intellij/spellchecker/engine/Metrics.java new file mode 100644 index 000000000000..c91a117cf90d --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/engine/Metrics.java @@ -0,0 +1,22 @@ +/* + * Copyright 2000-2009 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.intellij.spellchecker.engine; + + +public interface Metrics { + + int calculateMetrics(CharSequence str1, CharSequence str2); +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/engine/SpellChecker.java b/plugins/spellchecker/src/com/intellij/spellchecker/engine/SpellCheckerEngine.java similarity index 66% rename from plugins/spellchecker/src/com/intellij/spellchecker/engine/SpellChecker.java rename to plugins/spellchecker/src/com/intellij/spellchecker/engine/SpellCheckerEngine.java index e81a56e96de8..1afeb61ffb96 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/engine/SpellChecker.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/engine/SpellCheckerEngine.java @@ -1,53 +1,46 @@ -/* - * Copyright 2000-2009 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.intellij.spellchecker.engine; - -import org.jetbrains.annotations.NonNls; -import org.jetbrains.annotations.NotNull; - -import java.io.IOException; -import java.io.InputStream; -import java.util.List; -import java.util.Locale; - -/** - * Spell checker. - */ -public interface SpellChecker { - void addDictionary(@NotNull InputStream is, @NonNls String encoding, @NotNull Locale locale) throws IOException; - - void addToDictionary(@NotNull String word); - - void ignoreAll(@NotNull String word); - - boolean isIgnored(@NotNull String word); - - boolean isCorrect(@NotNull String word); - - @NotNull - List getSuggestions(@NotNull String word, int threshold); - - @NotNull - List getSuggestionsExt(@NotNull String word, int threshold); - - @NotNull - List getVariants(@NotNull String prefix); - - /** - * This method must clean up user dictionary words and ignored words. - */ - void reset(); -} +/* + * Copyright 2000-2009 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.intellij.spellchecker.engine; + +import com.intellij.spellchecker.dictionary.Loader; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + + +public interface SpellCheckerEngine { + + + void loadDictionary(@NotNull Loader loader); + + Transformation getTransformation(); + + boolean isCorrect(@NotNull String word); + + void addToDictionary(String word); + + @NotNull + List getSuggestions(@NotNull String word, int threshold, int quality); + + @NotNull + List getVariants(@NotNull String prefix); + + /** + * This method must clean up user dictionary words and ignored words. + */ + void reset(); + +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/engine/SpellCheckerFactory.java b/plugins/spellchecker/src/com/intellij/spellchecker/engine/SpellCheckerFactory.java index 06cfc7a6f490..f809ee7828a3 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/engine/SpellCheckerFactory.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/engine/SpellCheckerFactory.java @@ -1,28 +1,25 @@ -/* - * Copyright 2000-2009 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.intellij.spellchecker.engine; - -/** - * Spell checker factory. - */ -public final class SpellCheckerFactory { - private SpellCheckerFactory() { - } - - public static SpellChecker create() { - return new JazzySpellChecker(); - } -} +/* + * Copyright 2000-2009 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.intellij.spellchecker.engine; + +public final class SpellCheckerFactory { + private SpellCheckerFactory() { + } + + public static SpellCheckerEngine create() { + return new BaseSpellChecker(); + } +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/engine/Suggestion.java b/plugins/spellchecker/src/com/intellij/spellchecker/engine/Suggestion.java new file mode 100644 index 000000000000..85b0ee2a7060 --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/engine/Suggestion.java @@ -0,0 +1,65 @@ +/* + * Copyright 2000-2009 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.intellij.spellchecker.engine; + +public class Suggestion implements Comparable{ + private String word; + private int metrics; + + public Suggestion(String word, int metrics) { + this.word = word; + this.metrics = metrics; + } + + public String getWord() { + return word; + } + + + public int getMetrics() { + return metrics; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Suggestion result = (Suggestion)o; + + if (metrics != result.metrics) return false; + if (word != null ? !word.equals(result.word) : result.word != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = word != null ? word.hashCode() : 0; + result = 31 * result + metrics; + return result; + } + + + public int compareTo(Object o) { + if (!(o instanceof Suggestion)) throw new IllegalArgumentException(); + Suggestion r = (Suggestion)o; + return new Integer(getMetrics()).compareTo(r.getMetrics()); + } +} + + + diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/engine/Transformation.java b/plugins/spellchecker/src/com/intellij/spellchecker/engine/Transformation.java new file mode 100644 index 000000000000..b35d6e187088 --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/engine/Transformation.java @@ -0,0 +1,48 @@ +/* + * Copyright 2000-2009 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.intellij.spellchecker.engine; + +import com.intellij.util.containers.hash.HashSet; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.Set; + +public class Transformation { + + @Nullable + public String transform(@Nullable String word) { + if (word == null || word.trim().length() < 3) { + return null; + } + return word.trim().toLowerCase(); + } + + @Nullable + public Set transform(@Nullable Collection words) { + if (words == null || words.isEmpty()) { + return null; + } + Set result = new HashSet(); + for (String word : words) { + String transformed = transform(word); + if (transformed != null) { + result.add(transformed); + } + } + return result; + } +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/CheckArea.java b/plugins/spellchecker/src/com/intellij/spellchecker/inspections/CheckArea.java similarity index 85% rename from plugins/spellchecker/src/com/intellij/spellchecker/CheckArea.java rename to plugins/spellchecker/src/com/intellij/spellchecker/inspections/CheckArea.java index c3241600f840..e3075f94669e 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/CheckArea.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/inspections/CheckArea.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.intellij.spellchecker; +package com.intellij.spellchecker.inspections; import com.intellij.openapi.util.TextRange; import org.jetbrains.annotations.Nullable; @@ -39,17 +39,11 @@ public class CheckArea { return textRange; } - public void setTextRange(TextRange textRange) { - this.textRange = textRange; - } public boolean isIgnored() { return ignored; } - public void setIgnored(boolean ignored) { - this.ignored = ignored; - } @Nullable public String getWord() { @@ -59,6 +53,6 @@ public class CheckArea { @Override public String toString() { - return "CheckArea{range = " + textRange + ", ignored=" + ignored + ", word=" + (getWord()!=null?getWord():"") +'}'; + return "CheckArea{range = " + textRange + ", ignored=" + ignored + ", word=" + (getWord() != null ? getWord() : "") + '}'; } } diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/inspections/SpellCheckingInspection.java b/plugins/spellchecker/src/com/intellij/spellchecker/inspections/SpellCheckingInspection.java index cefe31399200..07459ca9cc14 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/inspections/SpellCheckingInspection.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/inspections/SpellCheckingInspection.java @@ -26,9 +26,7 @@ import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiElementVisitor; import com.intellij.psi.tree.IElementType; -import com.intellij.spellchecker.CheckArea; import com.intellij.spellchecker.SpellCheckerManager; -import com.intellij.spellchecker.TextSplitter; import com.intellij.spellchecker.quickfixes.AcceptWordAsCorrect; import com.intellij.spellchecker.quickfixes.ChangeTo; import com.intellij.spellchecker.quickfixes.RenameTo; @@ -180,7 +178,7 @@ public class SpellCheckingInspection extends LocalInspectionTool { fixes.add(new ChangeTo(textRange, word, token.getElement().getProject())); } else { - fixes.add(new RenameTo()); + fixes.add(new RenameTo(textRange, word, token.getElement().getProject())); } } @@ -201,6 +199,7 @@ public class SpellCheckingInspection extends LocalInspectionTool { final String description = tokenDescription == null ? defaultDescription : tokenDescription; final TextRange highlightRange = TextRange.from(token.getOffset() + textRange.getStartOffset(), textRange.getLength()); final LocalQuickFix[] quickFixes = fixes.size() > 0 ? fixes.toArray(new LocalQuickFix[fixes.size()]) : null; + return holder.getManager() .createProblemDescriptor(token.getElement(), highlightRange, description, ProblemHighlightType.GENERIC_ERROR_OR_WARNING, quickFixes); } diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/TextSplitter.java b/plugins/spellchecker/src/com/intellij/spellchecker/inspections/TextSplitter.java similarity index 98% rename from plugins/spellchecker/src/com/intellij/spellchecker/TextSplitter.java rename to plugins/spellchecker/src/com/intellij/spellchecker/inspections/TextSplitter.java index fb7a3dabe339..6f48b221e8c6 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/TextSplitter.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/inspections/TextSplitter.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.intellij.spellchecker; +package com.intellij.spellchecker.inspections; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.StringUtil; @@ -41,7 +41,7 @@ public class TextSplitter { @NonNls /*private static final Pattern WORD = Pattern.compile("\\b\\p{L}+'?\\p{L}*\\b");*/ - private static final Pattern WORD = Pattern.compile("\\b\\p{Alpha}*'?\\p{Alpha}"); + private static final Pattern WORD = Pattern.compile("\\b\\p{Alpha}*'?\\p{Alpha}*"); private static final Pattern EXTENDED_WORD = Pattern.compile("\\b\\p{Alpha}*'?\\p{Alpha}(_*\\p{Alpha})*"); diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/options/ProjectDictionaryState.java b/plugins/spellchecker/src/com/intellij/spellchecker/options/ProjectDictionaryState.java deleted file mode 100644 index 9e40c088ab7b..000000000000 --- a/plugins/spellchecker/src/com/intellij/spellchecker/options/ProjectDictionaryState.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2000-2009 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.intellij.spellchecker.options; - -import com.intellij.openapi.components.PersistentStateComponent; -import com.intellij.openapi.components.State; -import com.intellij.openapi.components.Storage; -import com.intellij.openapi.components.StorageScheme; -import com.intellij.util.xmlb.XmlSerializerUtil; -import com.intellij.util.xmlb.annotations.AbstractCollection; -import com.intellij.util.xmlb.annotations.Property; -import com.intellij.spellchecker.dictionary.UserDictionary; -import com.intellij.spellchecker.dictionary.Dictionary; -import com.intellij.spellchecker.dictionary.ProjectDictionary; - -import java.util.ArrayList; -import java.util.List; - -@State( - name = "ProjectDictionaryState", - storages = {@Storage( - id = "other", - file = "$PROJECT_FILE$"), @Storage( - id = "dir", - file = "$PROJECT_CONFIG_DIR$/dictionaries/", - scheme = StorageScheme.DIRECTORY_BASED, stateSplitter = ProjectDictionarySplitter.class)}) - -public class ProjectDictionaryState implements PersistentStateComponent { - - @Property(surroundWithTag = false) - @AbstractCollection(surroundWithTag = false,elementTypes = UserDictionary.class) - public List dictionaries = new ArrayList(); - - - private ProjectDictionary projectDictionary; - - - - public ProjectDictionaryState getState() { - return this; - } - - - public void loadState(ProjectDictionaryState state) { - XmlSerializerUtil.copyBean(state, this); - createProjectDictionary(); - } - - private void createProjectDictionary() { - projectDictionary = new ProjectDictionary("project"); - projectDictionary.setDictionaries(dictionaries); - projectDictionary.getUserDictionary(); - } - - public ProjectDictionary getDictionary() { - if (projectDictionary == null) { - createProjectDictionary(); - } - return projectDictionary; - } -} - diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/options/SpellCheckerConfigurable.java b/plugins/spellchecker/src/com/intellij/spellchecker/options/SpellCheckerConfigurable.java index 0ebbd1113ecf..57e5093a0b8d 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/options/SpellCheckerConfigurable.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/options/SpellCheckerConfigurable.java @@ -1,120 +1,102 @@ -/* - * Copyright 2000-2009 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.intellij.spellchecker.options; - -import com.intellij.openapi.options.Configurable; -import com.intellij.openapi.options.ConfigurationException; -import com.intellij.openapi.project.Project; -import com.intellij.spellchecker.SpellCheckerManager; -import com.intellij.spellchecker.util.SpellCheckerBundle; -import org.jetbrains.annotations.Nls; -import org.jetbrains.annotations.NonNls; -import org.jetbrains.annotations.Nullable; - -import javax.swing.*; -import java.util.Set; - - -public final class SpellCheckerConfigurable implements Configurable { - private SpellCheckerManager manager; - private final Project myProject; - private final SpellCheckerConfiguration configuration; - - private SpellCheckerOptions options; - - public SpellCheckerConfigurable(Project project, SpellCheckerConfiguration configuration) { - myProject = project; - this.configuration = configuration; - } - - @Nls - public String getDisplayName() { - return SpellCheckerBundle.message("spelling"); - } - - @Nullable - public Icon getIcon() { - return null; - } - - @Nullable - @NonNls - public String getHelpTopic() { - return "reference.settings.ide.settings.spelling"; - } - - public JComponent createComponent() { - manager = SpellCheckerManager.getInstance(myProject); - if (options == null) { - options = new SpellCheckerOptions(configuration, manager); - } - - return options.getRoot(); - } - - public boolean isModified() { - if (options != null) { - if (dictionaryListWasChanged()) { - return true; - } - - } - return false; - } - - - private boolean dictionaryListWasChanged() { - return !same(options.getUserDictionaryWordsSet(), options.getShownDictionary().getWords()); - } - - - private static boolean same(Set modified, Set original) { - if (original.size() != modified.size()) { - return false; - } - modified.removeAll(original); - return modified.size() == 0; - } - - public void apply() throws ConfigurationException { - if (options != null) { - boolean reload = false; - if (dictionaryListWasChanged()) { - options.getShownDictionary().replaceAllWords(options.getUserDictionaryWordsSet()); - reload = true; - } - - /***replaceAll(configuration.activeDictionary.dictionaryWords, options.getUserDictionaryWords()); - replaceAll(configuration.activeDictionary.ignoredWords, options.getIgnoredWords());*//**//* - manager.restartInspections();*/ - if (reload) { - manager.applyConfiguration(); - manager.restartInspections(); - } - } - } - - - public void reset() { - } - - public void disposeUIResources() { - if (options != null) { - options.dispose(); - options = null; - } - } -} +/* + * Copyright 2000-2009 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.intellij.spellchecker.options; + +import com.intellij.openapi.options.Configurable; +import com.intellij.openapi.options.ConfigurationException; +import com.intellij.openapi.project.Project; +import com.intellij.spellchecker.SpellCheckerManager; +import com.intellij.spellchecker.util.SpellCheckerBundle; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.util.List; +import java.util.Set; + + +public final class SpellCheckerConfigurable implements Configurable { + private SpellCheckerOptions options; + private SpellCheckerManager manager; + private final Project myProject; + + + public SpellCheckerConfigurable(Project project) { + myProject = project; + } + + @Nls + public String getDisplayName() { + return SpellCheckerBundle.message("spelling"); + } + + @Nullable + public Icon getIcon() { + return null; + } + + @Nullable + @NonNls + public String getHelpTopic() { + return "reference.settings.ide.settings.spelling"; + } + + public JComponent createComponent() { + manager = SpellCheckerManager.getInstance(myProject); + if (options == null) { + options = new SpellCheckerOptions(manager); + } + return options.getRoot(); + } + + public boolean isModified() { + if (options != null) { + return wordsListIsModified(); + } + return false; + } + + + private boolean wordsListIsModified() { + assert options != null; + List newWords = options.getWords(); + Set words = manager.getUserDictionary().getWords(); + if (words == null && newWords == null) { + return false; + } + if (words == null || newWords == null || newWords.size() != words.size()) { + return true; + } + return words.containsAll(newWords) && newWords.containsAll(words); + } + + + public void apply() throws ConfigurationException { + manager.updateUserWords(options.getWords()); + } + + + public void reset() { + } + + public void disposeUIResources() { + if (options != null) { + options.dispose(); + options = null; + } + } +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/options/SpellCheckerConfiguration.java b/plugins/spellchecker/src/com/intellij/spellchecker/options/SpellCheckerConfiguration.java deleted file mode 100644 index ed359e586ac9..000000000000 --- a/plugins/spellchecker/src/com/intellij/spellchecker/options/SpellCheckerConfiguration.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2000-2009 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.intellij.spellchecker.options; - -import com.intellij.openapi.components.PersistentStateComponent; -import com.intellij.openapi.components.State; -import com.intellij.openapi.components.Storage; -import com.intellij.openapi.components.StorageScheme; -import com.intellij.util.xmlb.XmlSerializerUtil; - -@State( - name = "spellchecker-configuration", - storages = { - @Storage( - id = "other", - file = "$PROJECT_FILE$"), - @Storage( - id = "dir", - file = "$PROJECT_CONFIG_DIR$/spellchecker.xml", - scheme = StorageScheme.DIRECTORY_BASED)}) -public final class SpellCheckerConfiguration implements PersistentStateComponent { - - - public SpellCheckerConfiguration getState() { - return this; - } - - public void loadState(SpellCheckerConfiguration state) { - XmlSerializerUtil.copyBean(state, this); - } - -} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/options/SpellCheckerOptions.form b/plugins/spellchecker/src/com/intellij/spellchecker/options/SpellCheckerOptions.form index 27582a3ad7a3..cb28418ead23 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/options/SpellCheckerOptions.form +++ b/plugins/spellchecker/src/com/intellij/spellchecker/options/SpellCheckerOptions.form @@ -1,70 +1,46 @@ - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/options/SpellCheckerOptions.java b/plugins/spellchecker/src/com/intellij/spellchecker/options/SpellCheckerOptions.java index b30b356a6eb0..a5412b93e260 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/options/SpellCheckerOptions.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/options/SpellCheckerOptions.java @@ -1,187 +1,169 @@ -/* - * Copyright 2000-2009 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.intellij.spellchecker.options; - -import com.intellij.openapi.Disposable; -import com.intellij.openapi.ui.Messages; -import com.intellij.spellchecker.dictionary.Dictionary; -import com.intellij.spellchecker.SpellCheckerManager; -import com.intellij.spellchecker.util.SpellCheckerBundle; -import com.intellij.spellchecker.util.Strings; -import com.intellij.ui.AddDeleteListPanel; -import com.intellij.util.containers.HashSet; - -import javax.swing.*; -import javax.swing.event.ChangeListener; -import javax.swing.event.ChangeEvent; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Set; - - -public class SpellCheckerOptions implements Disposable { - - private final SpellCheckerConfiguration configuration; - private final SpellCheckerManager manager; - - private JPanel root; - - private WordsPanel userDictionaryWords; - /*private WordsPanel ignoredWords;*/ - private JRadioButton projectRB; - private JRadioButton localRB; - private JLabel globalDictionaries; - - private Dictionary shownDictionary; - - public Dictionary getShownDictionary() { - return shownDictionary; - } - - public SpellCheckerOptions(SpellCheckerConfiguration configuration, SpellCheckerManager manager) { - this.configuration = configuration; - this.manager = manager; - } - - private void createUIComponents() { - userDictionaryWords = new WordsPanel(manager.getProjectWordList().getWords(), manager); - /*ignoredWords = new WordsPanel(manager.getProjectWordList().getIgnoredWords(), manager);*/ - shownDictionary = manager.getProjectWordList(); - - } - - public Set getUserDictionaryWordsSet() { - return getWords(userDictionaryWords); - } - - public boolean useProjectDictionary() { - return projectRB.isSelected(); - } - - public void setUserDictionaryWords(Set words) { - userDictionaryWords.replaceAll(words); - } - - /*public Set getIgnoredWords() { - return getWords(ignoredWords); - }*/ - - /* public void setIgnoredWords(Set dictionary) { - ignoredWords.replaceAll(dictionary); - }*/ - - - public JPanel getRoot() { - projectRB.setSelected(true); - projectRB.addChangeListener(new ChangeListener() { - public void stateChanged(ChangeEvent e) { - if (projectRB.isSelected()) { - shownDictionary = manager.getProjectWordList(); - userDictionaryWords.replaceAll(shownDictionary.getWords()); - /*ignoredWords.replaceAll(shownDictionary.getIgnoredWords());*/ - } - } - }); - localRB.addChangeListener(new ChangeListener() { - public void stateChanged(ChangeEvent e) { - if (localRB.isSelected()) { - shownDictionary = manager.getCachedWordList(); - userDictionaryWords.replaceAll(shownDictionary.getWords()); - /*ignoredWords.replaceAll(shownDictionary.getIgnoredWords());*/ - } - } - }); - - if (manager.getDictionaries() != null) { - String label = "Global dictionaries: "; - for (String dic : manager.getDictionaries()) { - label += dic + "; "; - } - globalDictionaries.setText(label); - } - return root; - } - - - private static Set getWords(AddDeleteListPanel panel) { - Set words = new HashSet(); - Object[] objects = panel.getListItems(); - for (Object object : objects) { - words.add((String)object); - } - return words; - } - - - public void dispose() { - userDictionaryWords.dispose(); - /*ignoredWords.dispose();*/ - } - - - private static final class WordsPanel extends AddDeleteListPanel implements Disposable { - private SpellCheckerManager manager; - - private WordsPanel(Set words, SpellCheckerManager manager) { - super(null, sort(words)); - this.manager = manager; - } - - private static List sort(Set words) { - List arrayList = new ArrayList(words); - Collections.sort(arrayList); - return arrayList; - } - - - - protected Object findItemToAdd() { - String word = - Messages.showInputDialog(SpellCheckerBundle.message("enter.simple.word"), SpellCheckerBundle.message("add.new.word"), null); - if (word == null) { - return null; - } - else { - word = word.trim(); - } - - if (Strings.isMixedCase(word)) { - Messages.showWarningDialog(SpellCheckerBundle.message("entered.word.0.is.mixed.cased.you.must.enter.simple.word", word), - SpellCheckerBundle.message("add.new.word")); - return null; - } - if (!manager.hasProblem(word)) { - Messages.showWarningDialog(SpellCheckerBundle.message("entered.word.0.is.correct.you.no.need.to.add.this.in.list", word), - SpellCheckerBundle.message("add.new.word")); - return null; - } - return word; - } - - public void replaceAll(Set words) { - myList.clearSelection(); - myListModel.removeAllElements(); - for (String word : sort(words)) { - myListModel.addElement(word); - } - } - - public void dispose() { - myListModel.removeAllElements(); - } - } -} +/* + * Copyright 2000-2009 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.intellij.spellchecker.options; + + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.ui.Messages; +import com.intellij.spellchecker.SpellCheckerManager; +import com.intellij.spellchecker.dictionary.Dictionary; +import com.intellij.spellchecker.util.SpellCheckerBundle; +import com.intellij.spellchecker.util.Strings; +import com.intellij.ui.AddDeleteListPanel; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + + +public class SpellCheckerOptions implements Disposable { + + private final SpellCheckerManager manager; + private JPanel root; + private JPanel wordPanelHolder; + private WordsPanel wordsPanel; + + + + public SpellCheckerOptions(SpellCheckerManager manager) { + this.manager = manager; + } + + public void createUIComponents() { + wordsPanel = new WordsPanel(manager); + + } + + public JPanel getRoot() { + return root; + } + + @Nullable + public List getWords(){ + Object[] pairs = wordsPanel.getListItems(); + if (pairs==null){ + return null; + } + List words = new ArrayList(); + for (Object pair : pairs) { + words.add(pair.toString()); + } + return words; + } + + public void dispose() { + + wordsPanel.dispose(); + } + + + public static final class WordDescriber { + private Dictionary dictionary; + + public WordDescriber(Dictionary dictionary) { + this.dictionary = dictionary; + } + + @NotNull + public List process() { + if (this.dictionary == null) { + return new ArrayList(); + } + Set words = this.dictionary.getEditableWords(); + if (words == null) { + return new ArrayList(); + } + List result = new ArrayList(); + for (String word : words) { + result.add(new Pair(word, "")); + } + Collections.sort(result); + return result; + } + } + + public static final class Pair implements Comparable { + private String word; + private String description; + + public Pair(@NotNull String word, String description) { + this.word = word; + this.description = description; + } + + public String getWord() { + return word; + } + + public String getDescription() { + return description; + } + + public int compareTo(Object o) { + if (!(o instanceof Pair)) { + throw new IllegalArgumentException(); + } + return word.compareTo(((Pair)o).getWord()); + } + + @Override + public String toString() { + return word + (description!=null && description.trim().length()>0?"("+description+")":""); + } + } + + private static final class WordsPanel extends AddDeleteListPanel implements Disposable { + private SpellCheckerManager manager; + + private WordsPanel(SpellCheckerManager manager) { + super(null, new WordDescriber(manager.getUserDictionary()).process()); + this.manager = manager; + } + + + protected Object findItemToAdd() { + String word = Messages.showInputDialog(com.intellij.spellchecker.util.SpellCheckerBundle.message("enter.simple.word"), + SpellCheckerBundle.message("add.new.word"), null); + if (word == null) { + return null; + } + else { + word = word.trim(); + } + + if (Strings.isMixedCase(word)) { + Messages.showWarningDialog(SpellCheckerBundle.message("entered.word.0.is.mixed.cased.you.must.enter.simple.word", word), + SpellCheckerBundle.message("add.new.word")); + return null; + } + if (!manager.hasProblem(word)) { + Messages.showWarningDialog(SpellCheckerBundle.message("entered.word.0.is.correct.you.no.need.to.add.this.in.list", word), + SpellCheckerBundle.message("add.new.word")); + return null; + } + return word; + } + + + public void dispose() { + myListModel.removeAllElements(); + } + } +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/AcceptWordAsCorrect.java b/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/AcceptWordAsCorrect.java index 499eb8351628..18a93ed00d80 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/AcceptWordAsCorrect.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/AcceptWordAsCorrect.java @@ -19,7 +19,7 @@ import com.intellij.codeInspection.ProblemDescriptor; import com.intellij.openapi.actionSystem.Anchor; import com.intellij.openapi.project.Project; import com.intellij.spellchecker.SpellCheckerManager; -import com.intellij.spellchecker.quickfixes.SpellCheckerQuickFix; + import com.intellij.spellchecker.util.SpellCheckerBundle; import org.jetbrains.annotations.NotNull; @@ -50,4 +50,5 @@ public class AcceptWordAsCorrect implements SpellCheckerQuickFix { SpellCheckerManager spellCheckerManager = SpellCheckerManager.getInstance(project); spellCheckerManager.acceptWordAsCorrect(word); } + } diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/ChangeTo.java b/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/ChangeTo.java index 407e24c57e58..b970c217e3d8 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/ChangeTo.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/ChangeTo.java @@ -26,25 +26,19 @@ import com.intellij.openapi.editor.Editor; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.StringUtil; -import com.intellij.spellchecker.SpellCheckerManager; import com.intellij.spellchecker.util.SpellCheckerBundle; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.List; -public class ChangeTo implements SpellCheckerQuickFix { - - private TextRange textRange; - private String word; - private Project project; +public class ChangeTo extends ShowSuggestions implements SpellCheckerQuickFix { public ChangeTo(@NotNull TextRange textRange, @NotNull String word, @NotNull Project project) { - this.textRange = textRange; - this.word = word; - this.project = project; + super(textRange, word, project); } + @NotNull public String getName() { return SpellCheckerBundle.message("change.to"); @@ -60,7 +54,7 @@ public class ChangeTo implements SpellCheckerQuickFix { return Anchor.FIRST; } - + public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { final Editor editor = PlatformDataKeys.EDITOR.getData(DataManager.getInstance().getDataContext()); @@ -78,11 +72,8 @@ public class ChangeTo implements SpellCheckerQuickFix { return; } - SpellCheckerManager manager = SpellCheckerManager.getInstance(project); - List variants = manager.getSuggestions(word); - List lookupItems = new ArrayList(); - for (String variant : variants) { + for (String variant : getSuggestions()) { lookupItems.add(LookupElementBuilder.create(variant)); } LookupElement[] items = new LookupElement[lookupItems.size()]; @@ -90,7 +81,6 @@ public class ChangeTo implements SpellCheckerQuickFix { LookupManager lookupManager = LookupManager.getInstance(project); lookupManager.showLookup(editor, items); - } diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/DictionarySuggestionProvider.java b/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/DictionarySuggestionProvider.java similarity index 96% rename from plugins/spellchecker/src/com/intellij/spellchecker/DictionarySuggestionProvider.java rename to plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/DictionarySuggestionProvider.java index 2f6288e6a88d..2e9ea068cae2 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/DictionarySuggestionProvider.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/DictionarySuggestionProvider.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.intellij.spellchecker; +package com.intellij.spellchecker.quickfixes; import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.psi.PsiElement; @@ -21,6 +21,7 @@ import com.intellij.psi.PsiNamedElement; import com.intellij.psi.codeStyle.NameUtil; import com.intellij.psi.codeStyle.SuggestedNameInfo; import com.intellij.refactoring.rename.NameSuggestionProvider; +import com.intellij.spellchecker.SpellCheckerManager; import java.util.*; diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/RenameTo.java b/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/RenameTo.java index e2264a1f2a0c..d9385e0fbc35 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/RenameTo.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/RenameTo.java @@ -20,10 +20,10 @@ import com.intellij.ide.DataManager; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.extensions.Extensions; import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; import com.intellij.refactoring.actions.RenameElementAction; import com.intellij.refactoring.rename.NameSuggestionProvider; -import com.intellij.spellchecker.DictionarySuggestionProvider; -import com.intellij.spellchecker.quickfixes.SpellCheckerQuickFix; +import com.intellij.spellchecker.quickfixes.DictionarySuggestionProvider; import com.intellij.spellchecker.util.SpellCheckerBundle; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -31,7 +31,11 @@ import org.jetbrains.annotations.Nullable; import javax.swing.*; -public class RenameTo implements SpellCheckerQuickFix { +public class RenameTo extends ShowSuggestions implements SpellCheckerQuickFix { + + public RenameTo(@NotNull TextRange textRange, @NotNull String word, @NotNull Project project) { + super(textRange, word, project); + } @NotNull public String getName() { diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/ShowSuggestions.java b/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/ShowSuggestions.java new file mode 100644 index 000000000000..d41972672461 --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/ShowSuggestions.java @@ -0,0 +1,58 @@ +/* + * Copyright 2000-2009 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.intellij.spellchecker.quickfixes; + +import com.intellij.codeInspection.LocalQuickFix; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; +import com.intellij.spellchecker.SpellCheckerManager; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + + +public abstract class ShowSuggestions implements LocalQuickFix { + + protected TextRange textRange; + protected String word; + protected Project project; + private List suggestions; + private boolean processed; + + + + + public ShowSuggestions(@NotNull TextRange textRange, @NotNull String word, @NotNull Project project) { + this.textRange = textRange; + this.word = word; + this.project = project; + } + + @NotNull + public List getSuggestions(){ + if (!processed){ + calculateSuggestions(); + processed=true; + } + return suggestions; + } + + private void calculateSuggestions(){ + SpellCheckerManager manager = SpellCheckerManager.getInstance(project); + suggestions = manager.getSuggestions(word); + } + +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/SpellCheckerQuickFix.java b/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/SpellCheckerQuickFix.java index 94fd0b8cbc64..9bb123f549fd 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/SpellCheckerQuickFix.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/SpellCheckerQuickFix.java @@ -19,16 +19,8 @@ import com.intellij.codeInspection.LocalQuickFix; import com.intellij.openapi.actionSystem.Anchor; import org.jetbrains.annotations.NotNull; -/** - * Spell checker quick fix. - */ public interface SpellCheckerQuickFix extends LocalQuickFix { - /** - * Return anchor for actions. Basically return {@link com.intellij.openapi.actionSystem.Anchor#FIRST} for common - * suggest words, and {@link com.intellij.openapi.actionSystem.Anchor#LAST} for 'Add to ...' actions. - * - * @return Action anchor - */ + @NotNull Anchor getPopupActionAnchor(); } diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/state/AggregatedDictionaryState.java b/plugins/spellchecker/src/com/intellij/spellchecker/state/AggregatedDictionaryState.java new file mode 100644 index 000000000000..c2fac874be88 --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/state/AggregatedDictionaryState.java @@ -0,0 +1,84 @@ +/* + * Copyright 2000-2009 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.intellij.spellchecker.state; + +import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.project.Project; +import com.intellij.spellchecker.dictionary.AggregatedDictionary; +import com.intellij.spellchecker.dictionary.ProjectDictionary; +import com.intellij.spellchecker.dictionary.UserDictionary; +import org.jetbrains.annotations.NotNull; + +public class AggregatedDictionaryState { + + private ProjectDictionaryState projectDictionaryState; + private CachedDictionaryState cachedDictionaryState; + private AggregatedDictionary dictionary; + private String currentUser; + private Project project; + + + public AggregatedDictionaryState() { + } + + public AggregatedDictionaryState(@NotNull AggregatedDictionary dictionary) { + setDictionary(dictionary); + } + + public void setProject(Project project) { + this.project = project; + } + + public void setCurrentUser(String currentUser) { + this.currentUser = currentUser; + } + + public void setDictionary(AggregatedDictionary dictionary) { + this.dictionary = dictionary; + cachedDictionaryState.setDictionary(dictionary.getCachedDictionary()); + projectDictionaryState.setProjectDictionary(dictionary.getProjectDictionary()); + } + + public AggregatedDictionary getDictionary() { + return dictionary; + } + + + + public void loadState() { + assert project != null; + cachedDictionaryState = ServiceManager.getService(project, CachedDictionaryState.class); + projectDictionaryState = ServiceManager.getService(project, ProjectDictionaryState.class); + currentUser = System.getProperty("user.name"); + retrieveDictionaries(); + } + + private void retrieveDictionaries() { + ProjectDictionary projectDictionary = projectDictionaryState.getProjectDictionary(); + projectDictionary.setActiveName(currentUser); + + if (cachedDictionaryState.getDictionary() == null) { + cachedDictionaryState.setDictionary(new UserDictionary(CachedDictionaryState.DEFAULT_NAME)); + } + dictionary = new AggregatedDictionary(projectDictionary, cachedDictionaryState.getDictionary()); + setDictionary(dictionary); + } + + @Override + public String toString() { + return "AggregatedDictionaryState{" + "dictionary=" + dictionary + '}'; + } +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/options/CachedDictionaryState.java b/plugins/spellchecker/src/com/intellij/spellchecker/state/CachedDictionaryState.java similarity index 62% rename from plugins/spellchecker/src/com/intellij/spellchecker/options/CachedDictionaryState.java rename to plugins/spellchecker/src/com/intellij/spellchecker/state/CachedDictionaryState.java index 5b3be8ebb8a9..0cc66939a1d9 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/options/CachedDictionaryState.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/state/CachedDictionaryState.java @@ -1,45 +1,47 @@ -/* - * Copyright 2000-2009 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.intellij.spellchecker.options; - -import com.intellij.openapi.components.PersistentStateComponent; -import com.intellij.openapi.components.State; -import com.intellij.openapi.components.Storage; -import com.intellij.util.xmlb.XmlSerializerUtil; -import com.intellij.spellchecker.dictionary.UserDictionary; -import com.intellij.spellchecker.dictionary.Dictionary; - -@State( - name = "CachedDictionaryState", - storages = {@Storage( - id = "spellchecker", - file = "$APP_CONFIG$/cachedDictionary.xml")}) -public class CachedDictionaryState implements PersistentStateComponent { - - public UserDictionary dictionary = new UserDictionary("cached"); - - public CachedDictionaryState getState() { - return this; - } - - public void loadState(CachedDictionaryState state) { - XmlSerializerUtil.copyBean(state, this); - } - - public Dictionary getDictionary(){ - return dictionary; - } -} +/* + * Copyright 2000-2009 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.intellij.spellchecker.state; + +import com.intellij.openapi.components.PersistentStateComponent; +import com.intellij.openapi.components.State; +import com.intellij.openapi.components.Storage; +import com.intellij.spellchecker.dictionary.Dictionary; + +@State( + name = "CachedDictionaryState", + storages = {@Storage( + id = "spellchecker", + file = "$APP_CONFIG$/cachedDictionary.xml")}) +public class CachedDictionaryState extends DictionaryState implements PersistentStateComponent{ + public static final String DEFAULT_NAME = "cached"; + + public CachedDictionaryState() { + name = DEFAULT_NAME; + } + + public CachedDictionaryState(Dictionary dictionary) { + super(dictionary); + name = DEFAULT_NAME; + } + + @Override + public void loadState(DictionaryState state) { + if (state.name==null){ + state.name= DEFAULT_NAME; + } + super.loadState(state); + } +} \ No newline at end of file diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/state/DictionaryState.java b/plugins/spellchecker/src/com/intellij/spellchecker/state/DictionaryState.java new file mode 100644 index 000000000000..857e7069f990 --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/state/DictionaryState.java @@ -0,0 +1,97 @@ +/* + * Copyright 2000-2009 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.intellij.spellchecker.state; + +import com.intellij.openapi.components.PersistentStateComponent; +import com.intellij.spellchecker.dictionary.Dictionary; +import com.intellij.spellchecker.dictionary.UserDictionary; +import com.intellij.util.xmlb.annotations.AbstractCollection; +import com.intellij.util.xmlb.annotations.Attribute; +import com.intellij.util.xmlb.annotations.Tag; +import com.intellij.util.xmlb.annotations.Transient; +import org.jetbrains.annotations.NotNull; + +import java.util.HashSet; +import java.util.Set; + +@Tag("dictionary") +public class DictionaryState implements PersistentStateComponent { + + public static final String NAME_ATTRIBUTE = "name"; + + @Tag("words") @AbstractCollection(surroundWithTag = false, elementTag = "w", elementValueAttribute = "") + public Set words = new HashSet(); + + @Attribute(NAME_ATTRIBUTE) + public String name; + + @Transient + private Dictionary dictionary; + + public DictionaryState() { + } + + public DictionaryState(@NotNull Dictionary dictionary) { + setDictionary(dictionary); + } + + @Transient + public void setDictionary(@NotNull Dictionary dictionary) { + this.dictionary = dictionary; + this.name = dictionary.getName(); + synchronizeWords(); + } + + @Transient + public Dictionary getDictionary() { + return dictionary; + } + + public DictionaryState getState() { + synchronizeWords(); + return this; + } + + private void synchronizeWords() { + if (dictionary != null) { + Set words = new HashSet(); + words.addAll(dictionary.getWords()); + this.words = words; + } + } + + public void loadState(DictionaryState state) { + if (state != null && state.name != null) { + name = state.name; + words = state.words; + } + retrieveDictionary(); + } + + private void retrieveDictionary() { + assert name != null; + dictionary = new UserDictionary(name); + dictionary.addToDictionary(words); + } + + + @Override + public String toString() { + return "DictionaryState{" + "dictionary=" + dictionary + '}'; + } + + +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/options/ProjectDictionarySplitter.java b/plugins/spellchecker/src/com/intellij/spellchecker/state/ProjectDictionarySplitter.java similarity index 86% rename from plugins/spellchecker/src/com/intellij/spellchecker/options/ProjectDictionarySplitter.java rename to plugins/spellchecker/src/com/intellij/spellchecker/state/ProjectDictionarySplitter.java index a0d1aeff7c74..dd2a5ff8d724 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/options/ProjectDictionarySplitter.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/state/ProjectDictionarySplitter.java @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.intellij.spellchecker.options; +package com.intellij.spellchecker.state; import com.intellij.openapi.components.StateSplitter; -import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.JDOMUtil; +import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.io.FileUtil; +import com.intellij.spellchecker.state.DictionaryState; import com.intellij.util.text.UniqueNameGenerator; import org.jdom.Element; @@ -36,8 +37,9 @@ public class ProjectDictionarySplitter implements StateSplitter { final UniqueNameGenerator generator = new UniqueNameGenerator(); List> result = new ArrayList>(); - for (Element element : JDOMUtil.getElements(e)) { - final String name = generator.generateUniqueName(FileUtil.sanitizeFileName(element.getAttributeValue("name"))) + ".xml"; + for (Element element : JDOMUtil.getElements(e)) { + final String name = generator.generateUniqueName(FileUtil.sanitizeFileName(element.getAttributeValue(DictionaryState.NAME_ATTRIBUTE))) + ".xml"; + result.add(new Pair(element, name)); } return result; diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/state/ProjectDictionaryState.java b/plugins/spellchecker/src/com/intellij/spellchecker/state/ProjectDictionaryState.java new file mode 100644 index 000000000000..97e996887049 --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/state/ProjectDictionaryState.java @@ -0,0 +1,118 @@ +/* + * Copyright 2000-2009 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.intellij.spellchecker.state; + +import com.intellij.openapi.components.PersistentStateComponent; +import com.intellij.openapi.components.State; +import com.intellij.openapi.components.Storage; +import com.intellij.openapi.components.StorageScheme; +import com.intellij.openapi.project.Project; +import com.intellij.spellchecker.dictionary.Dictionary; +import com.intellij.spellchecker.dictionary.ProjectDictionary; +import com.intellij.spellchecker.state.ProjectDictionarySplitter; +import com.intellij.util.containers.hash.HashSet; +import com.intellij.util.xmlb.annotations.AbstractCollection; +import com.intellij.util.xmlb.annotations.Property; +import com.intellij.util.xmlb.annotations.Transient; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +@State( + name = "ProjectDictionaryState", + storages = {@Storage( + id = "other", + file = "$PROJECT_FILE$"), + @Storage( + id = "dir", + file = "$PROJECT_CONFIG_DIR$/dictionaries/", + scheme = StorageScheme.DIRECTORY_BASED, stateSplitter = ProjectDictionarySplitter.class)}) + +public class ProjectDictionaryState implements PersistentStateComponent{ + + @Property(surroundWithTag = false) @AbstractCollection(surroundWithTag = false, elementTypes = DictionaryState.class) + public List dictionaryStates = new ArrayList(); + + + private ProjectDictionary projectDictionary; + private String currentUser; + private Project project; + + public ProjectDictionaryState() { + } + + public void setProject(Project project) { + this.project = project; + } + + public void setCurrentUser(String currentUser) { + this.currentUser = currentUser; + } + + + @Transient + public void setProjectDictionary(ProjectDictionary projectDictionary) { + currentUser = projectDictionary.getActiveName(); + dictionaryStates.clear(); + Set projectDictionaries = projectDictionary.getDictionaries(); + if (projectDictionaries != null) { + for (Dictionary dic : projectDictionary.getDictionaries()) { + dictionaryStates.add(new DictionaryState(dic)); + } + } + } + + @Transient + public ProjectDictionary getProjectDictionary() { + if (projectDictionary==null){ + projectDictionary = new ProjectDictionary(); + } + return projectDictionary; + } + + public ProjectDictionaryState getState() { + if (projectDictionary!=null){ + //ensure all dictionaries within project dictionary will be stored + setProjectDictionary(projectDictionary); + } + return this; + } + + + public void loadState(ProjectDictionaryState state) { + if (state != null) { + this.dictionaryStates = state.dictionaryStates; + } + retrieveProjectDictionaries(); + } + + private void retrieveProjectDictionaries() { + Set dictionaries = new HashSet(); + if (dictionaryStates != null) { + for (DictionaryState dictionaryState : dictionaryStates) { + dictionaryState.loadState(dictionaryState); + dictionaries.add(dictionaryState.getDictionary()); + } + } + projectDictionary = new ProjectDictionary(dictionaries); + } + + @Override + public String toString() { + return "ProjectDictionaryState{" + "projectDictionary=" + projectDictionary + '}'; + } +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/state/StateLoader.java b/plugins/spellchecker/src/com/intellij/spellchecker/state/StateLoader.java new file mode 100644 index 000000000000..7a9f9292012f --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/state/StateLoader.java @@ -0,0 +1,56 @@ +/* + * Copyright 2000-2009 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.intellij.spellchecker.state; + +import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.project.Project; +import com.intellij.spellchecker.dictionary.Dictionary; +import com.intellij.spellchecker.dictionary.Loader; +import com.intellij.spellchecker.dictionary.Processor; +import org.jetbrains.annotations.NotNull; + +import java.util.Set; + + +public class StateLoader implements Loader { + + private Project project; + private Dictionary dictionary; + + public StateLoader(Project project) { + this.project = project; + } + + + public void load(@NotNull Processor processor) { + AggregatedDictionaryState state = ServiceManager.getService(project, AggregatedDictionaryState.class); + state.setProject(project); + state.loadState(); + dictionary = state.getDictionary(); + final Set storedWords = dictionary.getWords(); + if (storedWords!=null){ + for (String word : storedWords) { + processor.process(word); + } + } + } + + public Dictionary getDictionary() { + return dictionary; + } +} + + diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/trie/Action.java b/plugins/spellchecker/src/com/intellij/spellchecker/trie/Action.java new file mode 100644 index 000000000000..4391ff8ca6e2 --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/trie/Action.java @@ -0,0 +1,23 @@ +/* + * Copyright 2000-2009 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.intellij.spellchecker.trie; + +import java.util.Map; + +public interface Action { + + void run(Map.Entry entry); +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/trie/CharSequenceKeyAnalyzer.java b/plugins/spellchecker/src/com/intellij/spellchecker/trie/CharSequenceKeyAnalyzer.java new file mode 100644 index 000000000000..a549b936f43c --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/trie/CharSequenceKeyAnalyzer.java @@ -0,0 +1,144 @@ +/* + * Copyright 2000-2009 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.intellij.spellchecker.trie; + + + +/** + * Analyzes CharSequence keys with case sensitivity. With + * CharSequenceKeyAnalyzer you can + * compare, check prefix, and determine the index of a bit. + *

+ * A typical use case for a CharSequenceKeyAnalyzer is with a + * {@link PatriciaTrie}. + *

+    PatriciaTrie<String, String> trie = new PatriciaTrie<String, String>(new CharSequenceKeyAnalyzer());
+
+    trie.put("Lime", "Lime");
+    trie.put("LimeWire", "LimeWire");
+    trie.put("LimeRadio", "LimeRadio");
+    trie.put("Lax", "Lax");
+    trie.put("Lake", "Lake");
+    trie.put("Lovely", "Lovely");
+
+    System.out.println(trie.select("Lo"));
+    System.out.println(trie.select("Lime"));
+
+    System.out.println(trie.getPrefixedBy("La").toString());
+
+    Output:
+        Lovely
+        Lime
+        {Lake=Lake, Lax=Lax}
+
+ * 
+ * + * @author Sam Berlin + * @author Roger Kapsi + */ +public class CharSequenceKeyAnalyzer implements PatriciaTrie.KeyAnalyzer { + + private static final long serialVersionUID = -7032449491269434877L; + + private static final int[] BITS = createIntBitMask(16); + + public static final int[] createIntBitMask(int bitCount) { + int[] bits = new int[bitCount]; + for(int i = 0; i < bitCount; i++) { + bits[i] = 1 << (bitCount - i - 1); + } + return bits; + } + public int length(CharSequence key) { + return (key != null ? key.length() * 16 : 0); + } + + public int bitIndex(CharSequence key, int keyOff, int keyLength, + CharSequence found, int foundOff, int foundKeyLength) { + boolean allNull = true; + + if(keyOff % 16 != 0 || foundOff % 16 != 0 || + keyLength % 16 != 0 || foundKeyLength % 16 != 0) + throw new IllegalArgumentException("offsets & lengths must be at character boundaries"); + + int off1 = keyOff / 16; + int off2 = foundOff / 16; + int len1 = keyLength / 16 + off1; + int len2 = foundKeyLength / 16 + off2; + int length = Math.max(len1, len2); + + // Look at each character, and if they're different + // then figure out which bit makes the difference + // and return it. + char k = 0, f = 0; + for(int i = 0; i < length; i++) { + int kOff = i + off1; + int fOff = i + off2; + + if(kOff >= len1) + k = 0; + else + k = key.charAt(kOff); + + if(found == null || fOff >= len2) + f = 0; + else + f = found.charAt(fOff); + + if(k != f) { + int x = k ^ f; + return i * 16 + (Integer.numberOfLeadingZeros(x) - 16); + } + + if(k != 0) + allNull = false; + + } + + if (allNull) { + return PatriciaTrie.KeyAnalyzer.NULL_BIT_KEY; + } + + return PatriciaTrie.KeyAnalyzer.EQUAL_BIT_KEY; + } + + public boolean isBitSet(CharSequence key, int keyLength, int bitIndex) { + if (key == null || bitIndex >= keyLength) { + return false; + } + + int index = bitIndex / BITS.length; + int bit = bitIndex - index * BITS.length; + return (key.charAt(index) & BITS[bit]) != 0; + } + + public int compare(CharSequence o1, CharSequence o2) { + return o1.toString().compareTo(o2.toString()); + } + + public int bitsPerElement() { + return 16; + } + + public boolean isPrefix(CharSequence prefix, int offset, int length, CharSequence key) { + if(offset % 16 != 0 || length % 16 != 0) + throw new IllegalArgumentException("Cannot determine prefix outside of character boundaries"); + String s1 = prefix.subSequence(offset / 16, length / 16).toString(); + String s2 = key.toString(); + return s2.startsWith(s1); + } +} + diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/trie/PatriciaTrie.java b/plugins/spellchecker/src/com/intellij/spellchecker/trie/PatriciaTrie.java new file mode 100644 index 000000000000..9770c9d7aba1 --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/trie/PatriciaTrie.java @@ -0,0 +1,2254 @@ +/* + * Copyright 2000-2009 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.intellij.spellchecker.trie; + + +import java.io.Serializable; +import java.util.AbstractCollection; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Comparator; +import java.util.ConcurrentModificationException; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.SortedMap; + +/** + * A PATRICIA Trie. + *

+ * PATRICIA = Practical Algorithm to Retrieve Information Coded in Alphanumeric + *

+ * A PATRICIA Trie is a compressed Trie. Instead of storing all data at the + * edges of the Trie (and having empty internal nodes), PATRICIA stores data + * in every node. This allows for very efficient traversal, insert, delete, + * predecessor, successor, prefix, range, and 'select' operations. All operations + * are performed at worst in O(K) time, where K is the number of bits in the + * largest item in the tree. In practice, operations actually take O(A(K)) + * time, where A(K) is the average number of bits of all items in the tree. + *

+ * Most importantly, PATRICIA requires very few comparisons to keys while + * doing any operation. While performing a lookup, each comparison + * (at most K of them, described above) will perform a single bit comparison + * against the given key, instead of comparing the entire key to another key. + *

+ * The Trie can return operations in lexicographical order using the 'traverse', + * 'prefix', 'submap', or 'iterator' methods. The Trie can also scan for items + * that are 'bitwise' (using an XOR metric) by the 'select' method. Bitwise + * closeness is determined by the {@link KeyAnalyzer} returning true or + * false for a bit being set or not in a given key. + *

+ * This PATRICIA Trie supports both variable length & fixed length keys. + * Some methods, such as getPrefixedBy(...) are suited only to + * variable length keys, whereas getPrefixedByBits(...) is suited + * to fixed-size keys. + *

+ * Additionally see PATRICIA + * for more information. + *

+ * Any methods here that take an Object may throw a + * ClassCastException if the method is expecting an instance of K + * (and it isn't K). + * + *

+    PatriciaTrie<String, String> trie = new PatriciaTrie<String, String>
+    (new CharSequenceKeyAnalyzer());
+
+    trie.put("Lime", "Lime");
+    trie.put("LimeWire", "LimeWire");
+    trie.put("LimeRadio", "LimeRadio");
+    trie.put("Lax", "Lax");
+    trie.put("Lake", "Lake");
+    trie.put("Lovely", "Lovely");
+
+    System.out.println(trie.select("Lo"));
+    System.out.println(trie.select("Lime"));
+
+    System.out.println(trie.getPrefixedBy("La").toString());
+
+    Output:
+        Lovely
+        Lime
+        {Lake=Lake, Lax=Lax}
+
+ * 
+ * @author Roger Kapsi + * @author Sam Berlin + + */ +public class PatriciaTrie extends AbstractMap implements Trie, Serializable { + + private static final long serialVersionUID = 110232526181493307L; + + /** The root element of the Trie. */ + private final TrieEntry root = new TrieEntry(null, null, -1); + + /** The current size (total number of elements) of the Trie. */ + private int size = 0; + + /** The number of times this has been modified (to fail-fast the iterators). */ + private transient int modCount = 0; + + /** The keyAnalyzer used to analyze bit values of keys. */ + private final KeyAnalyzer keyAnalyzer; + + /** Constructs a new PatriciaTrie using the given keyAnalyzer. */ + public PatriciaTrie(KeyAnalyzer keyAnalyzer) { + this.keyAnalyzer = keyAnalyzer; + } + + /** Returns the KeyAnalyzer that constructed the trie. */ + public KeyAnalyzer getKeyAnalyzer() { + return keyAnalyzer; + } + + /** Returns the KeyAnalyzer as a comparator. */ + public Comparator comparator() { + return keyAnalyzer; + } + + /** Clears the Trie (i.e. removes all elements). */ + public void clear() { + root.key = null; + root.bitIndex = -1; + root.value = null; + + root.parent = null; + root.left = root; + root.right = null; + root.predecessor = root; + + size = 0; + incrementModCount(); + } + + /** Returns true if the Trie is empty */ + public boolean isEmpty() { + return size == 0; + } + + /** Returns the number items in the Trie */ + public int size() { + return size; + } + + /** Increments both the size and mod counter. */ + private void incrementSize() { + size++; + incrementModCount(); + } + + /** Decrements the size and increments the mod counter. */ + private void decrementSize() { + size--; + incrementModCount(); + } + + /** Increments the mod counter. */ + private void incrementModCount() { + modCount++; + } + + /** + * Adds a new pair to the Trie and if a pair already + * exists it will be replaced. In the latter case it will return + * the old value. + */ + public V put(K key, V value) { + if (key == null) { + throw new NullPointerException("Key cannot be null"); + } + + int keyLength = length(key); + + // The only place to store a key with a length + // of zero bits is the root node + if (keyLength == 0) { + if (root.isEmpty()) { + incrementSize(); + } else { + incrementModCount(); + } + return root.setKeyValue(key, value); + } + + TrieEntry found = getNearestEntryForKey(key, keyLength); + if (key.equals(found.key)) { + if (found.isEmpty()) { // <- must be the root + incrementSize(); + } else { + incrementModCount(); + } + return found.setKeyValue(key, value); + } + + int bitIndex = bitIndex(key, found.key); + if (isValidBitIndex(bitIndex)) { // in 99.999...9% the case + /* NEW KEY+VALUE TUPLE */ + TrieEntry t = new TrieEntry(key, value, bitIndex); + addEntry(t, keyLength); + incrementSize(); + return null; + } else if (isNullBitKey(bitIndex)) { + // A bits of the Key are zero. The only place to + // store such a Key is the root Node! + + /* NULL BIT KEY */ + if (root.isEmpty()) { + incrementSize(); + } else { + incrementModCount(); + } + return root.setKeyValue(key, value); + + } else if (isEqualBitKey(bitIndex)) { + // This is a very special and rare case. + + /* REPLACE OLD KEY+VALUE */ + if (found != root) { + incrementModCount(); + return found.setKeyValue(key, value); + } + } + + throw new IndexOutOfBoundsException("Failed to put: " + key + " -> " + value + ", " + bitIndex); + } + + /** Adds the given entry into the Trie. */ + private TrieEntry addEntry(TrieEntry toAdd, int keyLength) { + TrieEntry current = root.left; + TrieEntry path = root; + while(true) { + if(current.bitIndex >= toAdd.bitIndex || current.bitIndex <= path.bitIndex) { + toAdd.predecessor = toAdd; + + if (!isBitSet(toAdd.key, keyLength, toAdd.bitIndex)) { + toAdd.left = toAdd; + toAdd.right = current; + } else { + toAdd.left = current; + toAdd.right = toAdd; + } + + toAdd.parent = path; + if (current.bitIndex >= toAdd.bitIndex) { + current.parent = toAdd; + } + + // if we inserted an uplink, set the predecessor on it + if(current.bitIndex <= path.bitIndex) { + current.predecessor = toAdd; + } + + if (path == root || !isBitSet(toAdd.key, keyLength, path.bitIndex)) + path.left = toAdd; + else + path.right = toAdd; + return toAdd; + } + + path = current; + if(!isBitSet(toAdd.key, keyLength, current.bitIndex)) + current = current.left; + else + current = current.right; + } + } + + @Override + public Set> entrySet() { + Set> es = entrySet; + return (es != null ? es : (entrySet = new EntrySet())); + } + + /** + * Returns the Value whose Key equals our lookup Key + * or null if no such key exists. + */ + public V get(Object k) { + TrieEntry entry = getEntry(k); + return entry != null ? entry.getValue() : null; + } + + /** + * Returns the entry associated with the specified key in the + * PatriciaTrie. Returns null if the map contains no mapping + * for this key. + * + * This may throw ClassCastException if the object is not of type K. + */ + TrieEntry getEntry(Object k) { + K key = asKey(k); + if(key == null) + return null; + + int keyLength = length(key); + TrieEntry entry = getNearestEntryForKey(key, keyLength); + return !entry.isEmpty() && key.equals(entry.key) ? entry : null; + } + + /** Gets the key as a 'K'. */ + @SuppressWarnings("unchecked") + protected final K asKey(Object key) { + try { + return (K)key; + } catch(ClassCastException cce) { + // Because the type is erased, the cast & return are + // actually doing nothing, making this CCE impossible. + // However, it's still here on the off-chance it may + // work. + return null; + } + } + + /** + * Returns the nearest entry for a given key. This is useful + * for finding knowing if a given key exists (and finding the value + * for it), or for inserting the key. + * + * The actual get implementation. This is very similar to + * selectR but with the exception that it might return the + * root Entry even if it's empty. + */ + private TrieEntry getNearestEntryForKey(K key, int keyLength) { + TrieEntry current = root.left; + TrieEntry path = root; + while(true) { + if(current.bitIndex <= path.bitIndex) + return current; + + path = current; + if(!isBitSet(key, keyLength, current.bitIndex)) + current = current.left; + else + current = current.right; + } + } + + /** + * Returns the Value whose Key has the longest prefix + * in common with our lookup key. + */ + @SuppressWarnings("unchecked") + public V select(K key) { + int keyLength = length(key); + TrieEntry[] result = new TrieEntry[1]; + if (!selectR(root.left, -1, key, keyLength, result)) { + TrieEntry e = result[0]; + return e.getValue(); + } + return null; + } + + /** + * This is equivalent to the other selectR() method but without + * its overhead because we're selecting only one best matching + * Entry from the Trie. + */ + private boolean selectR(TrieEntry h, int bitIndex, + final K key, final int keyLength, final TrieEntry[] result) { + + if (h.bitIndex <= bitIndex) { + // If we hit the root Node and it is empty + // we have to look for an alternative best + // matching node. + if (!h.isEmpty()) { + result[0] = h; + return false; + } + return true; + } + + if (!isBitSet(key, keyLength, h.bitIndex)) { + if (selectR(h.left, h.bitIndex, key, keyLength, result)) { + return selectR(h.right, h.bitIndex, key, keyLength, result); + } + } else { + if (selectR(h.right, h.bitIndex, key, keyLength, result)) { + return selectR(h.left, h.bitIndex, key, keyLength, result); + } + } + return false; + } + + @SuppressWarnings("unchecked") + public Map.Entry select(K key, Cursor cursor) { + int keyLength = length(key); + TrieEntry[] result = new TrieEntry[]{ null }; + selectR(root.left, -1, key, keyLength, cursor, result); + return result[0]; + } + + private boolean selectR(TrieEntry h, int bitIndex, + final K key, + final int keyLength, + final Cursor cursor, + final TrieEntry[] result) { + + if (h.bitIndex <= bitIndex) { + if(!h.isEmpty()) { + Cursor.SelectStatus ret = cursor.select(h); + switch(ret) { + case REMOVE: + throw new UnsupportedOperationException("cannot remove during select"); + case EXIT: + result[0] = h; + return false; // exit + case REMOVE_AND_EXIT: + TrieEntry entry = new TrieEntry(h.getKey(), h.getValue(), -1); + result[0] = entry; + removeEntry(h); + return false; + case CONTINUE: + // fall through. + } + } + return true; // continue + } + + if (!isBitSet(key, keyLength, h.bitIndex)) { + if (selectR(h.left, h.bitIndex, key, keyLength, cursor, result)) { + return selectR(h.right, h.bitIndex, key, keyLength, cursor, result); + } + } else { + if (selectR(h.right, h.bitIndex, key, keyLength, cursor, result)) { + return selectR(h.left, h.bitIndex, key, keyLength, cursor, result); + } + } + + return false; + } + + /** + * Returns a view of this Trie of all elements that are + * prefixed by the given key. + * + * In a fixed-keysize Trie, this is essentially a 'get' operation. + * + * For example, if the trie contains 'Lime', 'LimeWire', + * 'LimeRadio', 'Lax', 'Later', 'Lake', and 'Lovely', then + * a lookup of 'Lime' would return 'Lime', 'LimeRadio', and 'LimeWire'. + * + * The view that this returns is optimized to have a very efficient + * Iterator. The firstKey, lastKey & size methods must iterate + * over all possible values in order to determine the results. This + * information is cached until the Patricia tree changes. All other + * methods (except Iterator) must compare the given key to the prefix + * to ensure that it is within the range of the view. The Iterator's + * remove method must also relocate the subtree that contains the + * prefixes if the entry holding the subtree is removed or changes. + * Changing the subtree takes O(K) time. + * + * @param key + */ + public SortedMap getPrefixedBy(K key) { + return getPrefixedByBits(key, 0, keyAnalyzer.length(key)); + } + + /** + * Returns a view of this Trie of all elements that are + * prefixed by the length of the key. + * + * Fixed-keysize Tries will not support this operation + * (because all keys will be the same length). + * + * For example, if the trie contains 'Lime', 'LimeWire', + * 'LimeRadio', 'Lax', 'Later', 'Lake', and 'Lovely', then + * a lookup of 'LimePlastics' with a length of 4 would + * return 'Lime', 'LimeRadio', and 'LimeWire'. + * + * The view that this returns is optimized to have a very efficient + * Iterator. The firstKey, lastKey & size methods must iterate + * over all possible values in order to determine the results. This + * information is cached until the Patricia tree changes. All other + * methods (except Iterator) must compare the given key to the prefix + * to ensure that it is within the range of the view. The Iterator's + * remove method must also relocate the subtree that contains the + * prefixes if the entry holding the subtree is removed or changes. + * Changing the subtree takes O(K) time. + * + * @param key + * @param length + */ + public SortedMap getPrefixedBy(K key, int length) { + return getPrefixedByBits(key, 0, length * keyAnalyzer.bitsPerElement()); + } + + /** + * Returns a view of this Trie of all elements that are prefixed + * by the key, starting at the given offset and for the given length. + * + * Fixed-keysize Tries will not support this operation + * (because all keys are the same length). + * + * For example, if the trie contains 'Lime', 'LimeWire', + * 'LimeRadio', 'Lax', 'Later', 'Lake', and 'Lovely', then + * a lookup of 'The Lime Plastics' with an offset of 4 and a + * length of 4 would return 'Lime', 'LimeRadio', and 'LimeWire'. + * + * The view that this returns is optimized to have a very efficient + * Iterator. The firstKey, lastKey & size methods must iterate + * over all possible values in order to determine the results. This + * information is cached until the Patricia tree changes. All other + * methods (except Iterator) must compare the given key to the prefix + * to ensure that it is within the range of the view. The Iterator's + * remove method must also relocate the subtree that contains the + * prefixes if the entry holding the subtree is removed or changes. + * Changing the subtree takes O(K) time. + * + * @param key + * @param offset + * @param length + */ + public SortedMap getPrefixedBy(K key, int offset, int length) { + return getPrefixedByBits(key, offset * keyAnalyzer.bitsPerElement(), length * keyAnalyzer.bitsPerElement()); + } + + /** + * Returns a view of this Trie of all elements that are prefixed + * by the number of bits in the given Key. + * + * Fixed-keysize Tries can support this operation as a way to do + * lookups of partial keys. That is, if the Trie is storing IP + * addresses, you can lookup all addresses that begin with + * '192.168' by providing the key '192.168.X.X' and a length of 16 + * would return all addresses that begin with '192.168'. + * + * The view that this returns is optimized to have a very efficient + * Iterator. The firstKey, lastKey & size methods must iterate + * over all possible values in order to determine the results. This + * information is cached until the Patricia tree changes. All other + * methods (except Iterator) must compare the given key to the prefix + * to ensure that it is within the range of the view. The Iterator's + * remove method must also relocate the subtree that contains the + * prefixes if the entry holding the subtree is removed or changes. + * Changing the subtree takes O(K) time. + * + * @param key + * @param bitLength + */ + public SortedMap getPrefixedByBits(K key, int bitLength) { + return getPrefixedByBits(key, 0, bitLength); + } + + /** + * Returns a view of this map, with entries containing only those that + * are prefixed by a value whose bits matches the bits between 'offset' + * and 'length' in the given key. + * + * The view that this returns is optimized to have a very efficient + * Iterator. The firstKey, lastKey & size methods must iterate + * over all possible values in order to determine the results. This + * information is cached until the Patricia tree changes. All other + * methods (except Iterator) must compare the given key to the prefix + * to ensure that it is within the range of the view. The Iterator's + * remove method must also relocate the subtree that contains the + * prefixes if the entry holding the subtree is removed or changes. + * Changing the subtree takes O(K) time. + * + * @param key + * @param offset + * @param length + */ + private SortedMap getPrefixedByBits(K key, int offset, int length) { + int offsetLength = offset + length; + if (offsetLength > length(key)) { + throw new IllegalArgumentException(offset + " + " + length + " > " + length(key)); + } + + if(offsetLength == 0) + return this; + + return new PrefixSubMap(key, offset, length); + } + + /** + * Returns true if this trie contains the specified Key + * + * This may throw ClassCastException if the object is not + * of type K. + */ + public boolean containsKey(Object k) { + K key = asKey(k); + if(key == null) + return false; + + int keyLength = length(key); + TrieEntry entry = getNearestEntryForKey(key, keyLength); + return !entry.isEmpty() && key.equals(entry.key); + } + + /** Returns true if this Trie contains the specified value. */ + public boolean containsValue(Object o) { + for(V v : values()) + if(valEquals(v, o)) + return true; + return false; + } + + + /** + * Removes a Key from the Trie if one exists + * + * This may throw ClassCastException if the object is not of type K. + * + * @param k the Key to delete + * @return Returns the deleted Value + */ + public V remove(Object k) { + K key = asKey(k); + if(key == null) + return null; + + int keyLength = length(key); + TrieEntry current = root.left; + TrieEntry path = root; + while(true) { + if(current.bitIndex <= path.bitIndex) { + if(!current.isEmpty() && key.equals(current.key)) + return removeEntry(current); + else + return null; + } + + path = current; + if(!isBitSet(key, keyLength, current.bitIndex)) + current = current.left; + else + current = current.right; + } + } + + /** + * Removes a single entry from the Trie. + * + * If we found a Key (Entry h) then figure out if it's + * an internal (hard to remove) or external Entry (easy + * to remove) + */ + private V removeEntry(TrieEntry h) { + if (h != root) { + if (h.isInternalNode()) { + removeInternalEntry(h); + } else { + removeExternalEntry(h); + } + } + + decrementSize(); + return h.setKeyValue(null, null); + } + + /** + * Removes an external entry from the Trie. + * + * If it's an external Entry then just remove it. + * This is very easy and straight forward. + */ + private void removeExternalEntry(TrieEntry h) { + if (h == root) { + throw new IllegalArgumentException("Cannot delete root Entry!"); + } else if (!h.isExternalNode()) { + throw new IllegalArgumentException(h + " is not an external Entry!"); + } + + TrieEntry parent = h.parent; + TrieEntry child = (h.left == h) ? h.right : h.left; + + if (parent.left == h) { + parent.left = child; + } else { + parent.right = child; + } + + // either the parent is changing, or the predecessor is changing. + if (child.bitIndex > parent.bitIndex) { + child.parent = parent; + } else { + child.predecessor = parent; + } + + } + + /** + * Removes an internal entry from the Trie. + * + * If it's an internal Entry then "good luck" with understanding + * this code. The Idea is essentially that Entry p takes Entry h's + * place in the trie which requires some re-wiring. + */ + private void removeInternalEntry(TrieEntry h) { + if (h == root) { + throw new IllegalArgumentException("Cannot delete root Entry!"); + } else if (!h.isInternalNode()) { + throw new IllegalArgumentException(h + " is not an internal Entry!"); + } + + TrieEntry p = h.predecessor; + + // Set P's bitIndex + p.bitIndex = h.bitIndex; + + // Fix P's parent, predecessor and child Nodes + { + TrieEntry parent = p.parent; + TrieEntry child = (p.left == h) ? p.right : p.left; + + // if it was looping to itself previously, + // it will now be pointed from it's parent + // (if we aren't removing it's parent -- + // in that case, it remains looping to itself). + // otherwise, it will continue to have the same + // predecessor. + if(p.predecessor == p && p.parent != h) + p.predecessor = p.parent; + + if (parent.left == p) { + parent.left = child; + } else { + parent.right = child; + } + + if (child.bitIndex > parent.bitIndex) { + child.parent = parent; + } + }; + + // Fix H's parent and child Nodes + { + // If H is a parent of its left and right child + // then change them to P + if (h.left.parent == h) { + h.left.parent = p; + } + + if (h.right.parent == h) { + h.right.parent = p; + } + + // Change H's parent + if (h.parent.left == h) { + h.parent.left = p; + } else { + h.parent.right = p; + } + }; + + // Copy the remaining fields from H to P + //p.bitIndex = h.bitIndex; + p.parent = h.parent; + p.left = h.left; + p.right = h.right; + + // Make sure that if h was pointing to any uplinks, + // p now points to them. + if(isValidUplink(p.left, p)) + p.left.predecessor = p; + if(isValidUplink(p.right, p)) + p.right.predecessor = p; + + } + + /** + * Returns the node lexicographically before the given node (or null if none). + * + * This follows four simple branches: + * - If the uplink that returned us was a right uplink: + * - If predecessor's left is a valid uplink from predecessor, return it. + * - Else, follow the right path from the predecessor's left. + * - If the uplink that returned us was a left uplink: + * - Loop back through parents until we encounter a node where + * node != node.parent.left. + * - If node.parent.left is uplink from node.parent: + * - If node.parent.left is not root, return it. + * - If it is root & root isEmpty, return null. + * - If it is root & root !isEmpty, return root. + * - If node.parent.left is not uplink from node.parent: + * - Follow right path for first right child from node.parent.left + * + * @param start + */ + private TrieEntry previousEntry(TrieEntry start) { + if(start.predecessor == null) + throw new IllegalArgumentException("must have come from somewhere!"); + + if(start.predecessor.right == start) { + if(isValidUplink(start.predecessor.left, start.predecessor)) { + return start.predecessor.left; + } else { + return followRight(start.predecessor.left); + } + } else { + TrieEntry node = start.predecessor; + while(node.parent != null && node == node.parent.left) + node = node.parent; + if(node.parent == null) // can be null if we're looking up root. + return null; + if(isValidUplink(node.parent.left, node.parent)) { + if(node.parent.left == root) { + if(root.isEmpty()) + return null; + else + return root; + } else { + return node.parent.left; + } + } else { + return followRight(node.parent.left); + } + } + } + + /** + * Returns the entry lexicographically after the given entry. + * If the given entry is null, returns the first node. + */ + private TrieEntry nextEntry(TrieEntry node) { + if(node == null) { + return firstEntry(); + } else { + return nextEntryImpl(node.predecessor, node, null); + } + } + + /** + * Returns the entry lexicographically after the given entry. + * If the given entry is null, returns the first node. + * + * This will traverse only within the subtree. If the given node + * is not within the subtree, this will have undefined results. + */ + private TrieEntry nextEntryInSubtree(TrieEntry node, TrieEntry parentOfSubtree) { + if(node == null) { + return firstEntry(); + } else { + return nextEntryImpl(node.predecessor, node, parentOfSubtree); + } + } + + /** + * Scans for the next node, starting at the specified point, and using 'previous' + * as a hint that the last node we returned was 'previous' (so we know not to return + * it again). If 'tree' is non-null, this will limit the search to the given tree. + * + * The basic premise is that each iteration can follow the following steps: + * + * 1) Scan all the way to the left. + * a) If we already started from this node last time, proceed to Step 2. + * b) If a valid uplink is found, use it. + * c) If the result is an empty node (root not set), break the scan. + * d) If we already returned the left node, break the scan. + * + * 2) Check the right. + * a) If we already returned the right node, proceed to Step 3. + * b) If it is a valid uplink, use it. + * c) Do Step 1 from the right node. + * + * 3) Back up through the parents until we encounter find a parent + * that we're not the right child of. + * + * 4) If there's no right child of that parent, the iteration is finished. + * Otherwise continue to Step 5. + * + * 5) Check to see if the right child is a valid uplink. + * a) If we already returned that child, proceed to Step 6. + * Otherwise, use it. + * + * 6) If the right child of the parent is the parent itself, we've + * already found & returned the end of the Trie, so exit. + * + * 7) Do Step 1 on the parent's right child. + */ + private TrieEntry nextEntryImpl(TrieEntry start, TrieEntry previous, TrieEntry tree) { + TrieEntry current = start; + + // Only look at the left if this was a recursive or + // the first check, otherwise we know we've already looked + // at the left. + if(previous == null || start != previous.predecessor) { + while(!current.left.isEmpty()) { + // stop traversing if we've already + // returned the left of this node. + if(previous == current.left) { + break; + } + + if(isValidUplink(current.left, current)) { + return current.left; + } + + current = current.left; + } + } + + // If there's no data at all, exit. + if(current.isEmpty()) { + return null; + } + + // If we've already returned the left, + // and the immediate right is null, + // there's only one entry in the Trie + // which is stored at the root. + // + // / ("") <-- root + // \_/ \ + // null <-- 'current' + // + if(current.right == null) + return null; + + // If nothing valid on the left, try the right. + if(previous != current.right) { + // See if it immediately is valid. + if(isValidUplink(current.right, current)) { + return current.right; + } + + // Must search on the right's side if it wasn't initially valid. + return nextEntryImpl(current.right, previous, tree); + } + + // Neither left nor right are valid, find the first parent + // whose child did not come from the right & traverse it. + while(current == current.parent.right) { + // If we're going to traverse to above the subtree, stop. + if(current == tree) + return null; + + current = current.parent; + } + + // If we're on the top of the subtree, we can't go any higher. + if(current == tree) + return null; + + + // If there's no right, the parent must be root, so we're done. + if(current.parent.right == null) { + return null; + } + + + // If the parent's right points to itself, we've found one. + if(previous != current.parent.right && isValidUplink(current.parent.right, current.parent)) { + return current.parent.right; + } + + // If the parent's right is itself, there can't be any more nodes. + if(current.parent.right == current.parent) { + return null; + } + + // We need to traverse down the parent's right's path. + return nextEntryImpl(current.parent.right, previous, tree); + } + + /** Returns each entry as a string. */ + public String toString() { + StringBuilder buffer = new StringBuilder(); + buffer.append("Trie[").append(size()).append("]={\n"); + for(Iterator> i = newEntryIterator(); i.hasNext(); ) { + buffer.append(" ").append(i.next().toString()).append("\n"); + } + buffer.append("}\n"); + return buffer.toString(); + } + + public Map.Entry traverse(Cursor cursor) { + TrieEntry entry = nextEntry(null); + while(entry != null) { + TrieEntry current = entry; + Cursor.SelectStatus ret = cursor.select(current); + entry = nextEntry(current); + switch(ret) { + case EXIT: + return current; + case REMOVE: + removeEntry(current); + break; // out of switch, stay in while loop + case REMOVE_AND_EXIT: + Map.Entry value = new TrieEntry(current.getKey(), current.getValue(), -1); + removeEntry(current); + return value; + case CONTINUE: // do nothing. + } + } + + return null; + } + + /** Returns true if 'next' is a valid uplink coming from 'from'. */ + private boolean isValidUplink(TrieEntry next, TrieEntry from) { + return next != null && next.bitIndex <= from.bitIndex && !next.isEmpty(); + } + + /** Returns true if bitIndex is a valid index */ + private static boolean isValidBitIndex(int bitIndex) { + return 0 <= bitIndex && bitIndex <= Integer.MAX_VALUE; + } + + /** Returns true if bitIndex is a NULL_BIT_KEY */ + private static boolean isNullBitKey(int bitIndex) { + return bitIndex == KeyAnalyzer.NULL_BIT_KEY; + } + + /** Returns true if bitIndex is a EQUAL_BIT_KEY */ + private static boolean isEqualBitKey(int bitIndex) { + return bitIndex == KeyAnalyzer.EQUAL_BIT_KEY; + } + + /** Returns the length of the key, or 0 if the key is null. */ + private int length(K key) { + if (key == null) { + return 0; + } + + return keyAnalyzer.length(key); + } + + /** + * Returns whether or not the given bit on the + * key is set, or false if the key is null + */ + private boolean isBitSet(K key, int keyLength, int bitIndex) { + if (key == null) { // root's might be null! + return false; + } + return keyAnalyzer.isBitSet(key, keyLength, bitIndex); + } + + /** + * Utility method for calling + * keyAnalyzer.bitIndex(key, 0, length(key), foundKey, 0, length(foundKey)) + */ + private int bitIndex(K key, K foundKey) { + return keyAnalyzer.bitIndex(key, 0, length(key), foundKey, 0, length(foundKey)); + } + + /** The actual Trie nodes. */ + private static class TrieEntry implements Map.Entry, Serializable { + + private static final long serialVersionUID = 4596023148184140013L; + + private K key; + private V value; + + /** The index this entry is comparing. */ + private int bitIndex; + + /** The parent of this entry. */ + private TrieEntry parent; + /** The left child of this entry. */ + private TrieEntry left; + /** The right child of this entry. */ + private TrieEntry right; + /** The entry who uplinks to this entry. */ + private TrieEntry predecessor; + + private TrieEntry(K key, V value, int bitIndex) { + this.key = key; + this.value = value; + + this.bitIndex = bitIndex; + + this.parent = null; + this.left = this; + this.right = null; + this.predecessor = this; + } + + public boolean equals(Object o) { + if(o == this) { + return true; + } else if(o instanceof Map.Entry) { + Map.Entry e = (Map.Entry)o; + Object k1 = getKey(); + Object k2 = e.getKey(); + if (k1 == k2 || (k1 != null && k1.equals(k2))) { + Object v1 = getValue(); + Object v2 = e.getValue(); + if (v1 == v2 || (v1 != null && v1.equals(v2))) + return true; + } + return false; + } else { + return false; + } + } + + /** + * Whether or not the entry is storing a key. + * Only the root can potentially be empty, all other + * nodes must have a key. + */ + public boolean isEmpty() { + return key == null; + } + + public K getKey() { + return key; + } + + public V getValue() { + return value; + } + + public V setValue(V value) { + V o = this.value; + this.value = value; + return o; + } + + /** + * Replaces the old key and value with the new ones. + * Returns the old vlaue. + */ + private V setKeyValue(K key, V value) { + this.key = key; + return setValue(value); + } + + /** Neither the left nor right child is a loopback */ + private boolean isInternalNode() { + return left != this && right != this; + } + + /** Either the left or right child is a loopback */ + private boolean isExternalNode() { + return !isInternalNode(); + } + + public String toString() { + StringBuilder buffer = new StringBuilder(); + + if (bitIndex == -1) { + buffer.append("RootEntry("); + } else { + buffer.append("Entry("); + } + + buffer.append("key=").append(getKey()).append(" [").append(bitIndex).append("], "); + buffer.append("value=").append(getValue()).append(", "); + //buffer.append("bitIndex=").append(bitIndex).append(", "); + + if (parent != null) { + if (parent.bitIndex == -1) { + buffer.append("parent=").append("ROOT"); + } else { + buffer.append("parent=").append(parent.getKey()).append(" [").append(parent.bitIndex).append("]"); + } + } else { + buffer.append("parent=").append("null"); + } + buffer.append(", "); + + if (left != null) { + if (left.bitIndex == -1) { + buffer.append("left=").append("ROOT"); + } else { + buffer.append("left=").append(left.getKey()).append(" [").append(left.bitIndex).append("]"); + } + } else { + buffer.append("left=").append("null"); + } + buffer.append(", "); + + if (right != null) { + if (right.bitIndex == -1) { + buffer.append("right=").append("ROOT"); + } else { + buffer.append("right=").append(right.getKey()).append(" [").append(right.bitIndex).append("]"); + } + } else { + buffer.append("right=").append("null"); + } + buffer.append(", "); + + if(predecessor != null) { + if(predecessor.bitIndex == -1) { + buffer.append("predecessor=").append("ROOT"); + } else { + buffer.append("predecessor=").append(predecessor.getKey()).append(" [").append(predecessor.bitIndex).append("]"); + } + } + + buffer.append(")"); + return buffer.toString(); + } + } + + /** + * Defines the interface to analyze {@link Trie} keys on a bit + * level. KeyAnalyzer's + * methods return the length of the key in bits, whether or not a bit is + * set, and bits per element in the key. + *

+ * Additionally, a method determines if a key is a prefix of another key and + * returns the bit index where one key is different from another key (if + * the key and found key are equal than the return value is EQUAL_BIT_KEY). + *

+ * KeyAnalyzer defines:
+ * + * + * + *
NULL_BIT_KEYWhen key's bits are all zero
EQUAL_BIT_KEY When keys are the same
+ */ + public static interface KeyAnalyzer extends Comparator, Serializable { + + /** Returned by bitIndex if key's bits are all 0 */ + public static final int NULL_BIT_KEY = -1; + + /** + * Returned by bitIndex if key and found key are + * equal. This is a very very specific case and + * shouldn't happen on a regular basis + */ + public static final int EQUAL_BIT_KEY = -2; + + /** + * Returns the length of the Key in bits. + */ + public int length(K key); + + /** Returns whether or not a bit is set */ + public boolean isBitSet(K key, int keyLength, int bitIndex); + + /** + * Returns the n-th different bit between key and found. + * This starts the comparison in key at 'keyStart' and goes + * for 'keyLength' bits, and compares to the found key + * starting at 'foundStart' and going for 'foundLength' bits. + */ + public int bitIndex(K key, int keyStart, int keyLength, + K found, int foundStart, int foundLength); + + /** + * Returns the number of bits per element in the key. + * This is only useful for variable-length keys, such as Strings. + */ + public int bitsPerElement(); + + /** + * Determines whether or not the given prefix (from offset to length) + * is a prefix of the given key. + */ + public boolean isPrefix(K prefix, int offset, int length, K key); + } + + /** An iterator that stores a single TrieEntry. */ + private class SingletonIterator implements Iterator> { + private final TrieEntry entry; + private int hit = 0; + + public SingletonIterator(TrieEntry entry) { + this.entry = entry; + } + + public boolean hasNext() { + return hit == 0; + } + + public Map.Entry next() { + if(hit != 0) + throw new NoSuchElementException(); + hit++; + return entry; + } + + public void remove() { + if(hit != 1) + throw new IllegalStateException(); + hit++; + PatriciaTrie.this.removeEntry(entry); + } + + } + + /** An iterator for the entries. */ + private abstract class NodeIterator implements Iterator { + protected int expectedModCount = modCount; // For fast-fail + protected TrieEntry next; // the next node to return + protected TrieEntry current; // the current entry we're on + + // Starts iteration from the beginning. + protected NodeIterator() { + next = PatriciaTrie.this.nextEntry(null); + } + + // Starts iteration at the given entry. + protected NodeIterator(TrieEntry firstEntry) { + next = firstEntry; + } + + public boolean hasNext() { + return next != null; + } + + TrieEntry nextEntry() { + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + TrieEntry e = next; + if (e == null) + throw new NoSuchElementException(); + + next = findNext(e); + current = e; + return e; + } + + protected TrieEntry findNext(TrieEntry prior) { + return PatriciaTrie.this.nextEntry(prior); + } + + public void remove() { + if (current == null) + throw new IllegalStateException(); + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + + TrieEntry node = current; + current = null; + PatriciaTrie.this.removeEntry(node); + + expectedModCount = modCount; + } + } + + private class ValueIterator extends NodeIterator { + public V next() { + return nextEntry().value; + } + } + + private class KeyIterator extends NodeIterator { + public K next() { + return nextEntry().getKey(); + } + } + + private class EntryIterator extends NodeIterator> { + public Map.Entry next() { + return nextEntry(); + } + } + + /** An iterator for iterating over a prefix search. */ + private class PrefixEntryIterator extends NodeIterator> { + // values to reset the subtree if we remove it. + protected final K prefix; + protected final int offset; + protected final int length; + protected boolean lastOne; + + protected TrieEntry subtree; // the subtree to search within + + // Starts iteration at the given entry & search only within the given subtree. + PrefixEntryIterator(TrieEntry startScan, K prefix, int offset, int length) { + subtree = startScan; + next = PatriciaTrie.this.followLeft(startScan); + this.prefix = prefix; + this.offset = offset; + this.length = length; + } + + public Map.Entry next() { + Map.Entry entry = nextEntry(); + if(lastOne) + next = null; + return entry; + } + + @Override + protected TrieEntry findNext(TrieEntry prior) { + return PatriciaTrie.this.nextEntryInSubtree(prior, subtree); + } + + @Override + public void remove() { + // If the current entry we're removing is the subtree + // then we need to find a new subtree parent. + boolean needsFixing = false; + int bitIdx = subtree.bitIndex; + if(current == subtree) + needsFixing = true; + + super.remove(); + + // If the subtree changed its bitIndex or we + // removed the old subtree, get a new one. + if(bitIdx != subtree.bitIndex || needsFixing) + subtree = subtree(prefix, offset, length); + + // If the subtree's bitIndex is less than the + // length of our prefix, it's the last item + // in the prefix tree. + if(length >= subtree.bitIndex) + lastOne = true; + } + + } + + /** An iterator for submaps. */ + private class SubMapEntryIterator extends NodeIterator> { + private final K firstExcludedKey; + + SubMapEntryIterator(TrieEntry first, TrieEntry firstExcluded) { + super(first); + firstExcludedKey = + (firstExcluded == null ? null : firstExcluded.key); + } + + public boolean hasNext() { + return next != null && next.key != firstExcludedKey; + } + + public Map.Entry next() { + if (next == null || next.key == firstExcludedKey) + throw new NoSuchElementException(); + return nextEntry(); + } + } + + Iterator newKeyIterator() { + return new KeyIterator(); + } + + Iterator newValueIterator() { + return new ValueIterator(); + } + + Iterator> newEntryIterator() { + return new EntryIterator(); + } + + /** + * Each of these fields are initialized to contain an instance of the + * appropriate view the first time this view is requested. The views are + * stateless, so there's no reason to create more than one of each. + */ + private transient volatile Set keySet = null; + private transient volatile Collection values = null; + private transient volatile Set> entrySet = null; + + private class EntrySet extends AbstractSet> { + public Iterator> iterator() { + return newEntryIterator(); + } + + public boolean contains(Object o) { + if (!(o instanceof Map.Entry)) + return false; + + TrieEntry candidate = getEntry(((Map.Entry)o).getKey()); + return candidate != null && candidate.equals(o); + } + + public boolean remove(Object o) { + int size = size(); + PatriciaTrie.this.remove(o); + return size != size(); + } + + public int size() { + return size; + } + + public void clear() { + PatriciaTrie.this.clear(); + } + } + + /** + * Returns a set view of the keys contained in this map. The set is + * backed by the map, so changes to the map are reflected in the set, and + * vice-versa. The set supports element removal, which removes the + * corresponding mapping from this map, via the Iterator.remove, + * Set.remove, removeAll, retainAll, and + * clear operations. It does not support the add or + * addAll operations. + * + * @return a set view of the keys contained in this map. + */ + public Set keySet() { + Set ks = keySet; + return (ks != null ? ks : (keySet = new KeySet())); + } + + private class KeySet extends AbstractSet { + public Iterator iterator() { + return newKeyIterator(); + } + public int size() { + return size; + } + public boolean contains(Object o) { + return containsKey(o); + } + public boolean remove(Object o) { + int size = size(); + PatriciaTrie.this.remove(o); + return size != size(); + } + public void clear() { + PatriciaTrie.this.clear(); + } + } + + /** + * Returns a collection view of the values contained in this map. The + * collection is backed by the map, so changes to the map are reflected in + * the collection, and vice-versa. The collection supports element + * removal, which removes the corresponding mapping from this map, via the + * Iterator.remove, Collection.remove, + * removeAll, retainAll, and clear operations. + * It does not support the add or addAll operations. + * + * @return a collection view of the values contained in this map. + */ + public Collection values() { + Collection vs = values; + return (vs != null ? vs : (values = new Values())); + } + + /** Test two values for equality. Works with null values. */ + private static boolean valEquals(Object o1, Object o2) { + return (o1==null ? o2==null : o1.equals(o2)); + } + + private class Values extends AbstractCollection { + public Iterator iterator() { + return newValueIterator(); + } + public int size() { + return size; + } + public boolean contains(Object o) { + return containsValue(o); + } + public void clear() { + PatriciaTrie.this.clear(); + } + public boolean remove(Object o) { + for(Iterator i = iterator(); i.hasNext(); ) { + V v = i.next(); + if(valEquals(v, o)) { + i.remove(); + return true; + } + } + return false; + } + } + + /** + * Returns the first entry the Trie is storing. + * + * This is implemented by going always to the left until + * we encounter a valid uplink. That uplink is the first key. + */ + private TrieEntry firstEntry() { + // if Trie is empty, no first node. + if(isEmpty()) + return null; + + return followLeft(root); + } + + /** Goes left through the tree until it finds a valid node. */ + private TrieEntry followLeft(TrieEntry node) { + while(true) { + TrieEntry child = node.left; + // if we hit root and it didn't have a node, go right instead. + if(child.isEmpty()) + child = node.right; + + if(child.bitIndex <= node.bitIndex) + return child; + + node = child; + } + } + + /** + * Returns the last entry the Trie is storing. + *

+ * This is implemented by going always to the right until + * we encounter a valid uplink. That uplink is the last key. + */ + private TrieEntry lastEntry() { + return followRight(root.left); + } + + /** Traverses down the right path until it finds an uplink. */ + protected TrieEntry followRight(TrieEntry node) { + // if Trie is empty, no last entry. + if(node.right == null) + return null; + + // Go as far right as possible, until we encounter an uplink. + while(node.right.bitIndex > node.bitIndex) + node = node.right; + + return node.right; + } + + public K firstKey() { + return firstEntry().getKey(); + } + + public SortedMap headMap(K toKey) { + return new SubMap(null, toKey); + } + + public K lastKey() { + TrieEntry entry = lastEntry(); + if(entry != null) + return entry.getKey(); + else + return null; + } + + + public SortedMap subMap(K fromKey, K toKey) { + return new SubMap(fromKey, toKey); + } + + public SortedMap tailMap(K fromKey) { + return new SubMap(fromKey, null); + } + + /** + * Returns an entry strictly higher than the given key, + * or null if no such entry exists. + */ + protected TrieEntry higherEntry(K key) { + // TODO: Cleanup so that we don't actually have to add/remove from the + // tree. (We do it here because there are other well-defined + // functions to perform the search.) + int keyLength = length(key); + + if (keyLength == 0) { + if(!root.isEmpty()) { + // If data in root, and more after -- return it. + if(size() > 1) { + return nextEntry(root); + } else { // If no more after, no higher entry. + return null; + } + } else { + // Root is empty & we want something after empty, return first. + return firstEntry(); + } + } + + TrieEntry found = getNearestEntryForKey(key, keyLength); + if (key.equals(found.key)) + return nextEntry(found); + + int bitIndex = bitIndex(key, found.key); + if (isValidBitIndex(bitIndex)) { + TrieEntry added = new TrieEntry(key, null, bitIndex); + addEntry(added, keyLength); + incrementSize(); // must increment because remove will decrement + TrieEntry ceil = nextEntry(added); + removeEntry(added); + modCount -= 2; // we didn't really modify it. + return ceil; + } else if (isNullBitKey(bitIndex)) { + if (!root.isEmpty()) + return firstEntry(); + else if(size() > 1) + return nextEntry(firstEntry()); + else + return null; + } else if (isEqualBitKey(bitIndex)) { + return nextEntry(found); + } + + // we should have exited above. + throw new IllegalStateException("invalid lookup: " + key); + } + + /** + * Returns a key-value mapping associated with the least key greater + * than or equal to the given key, or null if there is no such key. + */ + protected TrieEntry ceilingEntry(K key) { + // Basically: + // Follow the steps of adding an entry, but instead... + // + // - If we ever encounter a situation where we found an equal + // key, we return it immediately. + // + // - If we hit an empty root, return the first iterable item. + // + // - If we have to add a new item, we temporarily add it, + // find the successor to it, then remove the added item. + // + // These steps ensure that the returned value is either the + // entry for the key itself, or the first entry directly after + // the key. + + // TODO: Cleanup so that we don't actually have to add/remove from the + // tree. (We do it here because there are other well-defined + // functions to perform the search.) + int keyLength = length(key); + + if (keyLength == 0) { + if(!root.isEmpty()) + return root; + else + return firstEntry(); + } + + TrieEntry found = getNearestEntryForKey(key, keyLength); + if (key.equals(found.key)) + return found; + + int bitIndex = bitIndex(key, found.key); + if (isValidBitIndex(bitIndex)) { + TrieEntry added = new TrieEntry(key, null, bitIndex); + addEntry(added, keyLength); + incrementSize(); // must increment because remove will decrement + TrieEntry ceil = nextEntry(added); + removeEntry(added); + modCount -= 2; // we didn't really modify it. + return ceil; + } else if (isNullBitKey(bitIndex)) { + if (!root.isEmpty()) + return root; + else + return firstEntry(); + } else if (isEqualBitKey(bitIndex)) { + return found; + } + + // we should have exited above. + throw new IllegalStateException("invalid lookup: " + key); + } + + /** + * Returns a key-value mapping associated with the greatest key + * strictly less than the given key, or null if there is no such key. + */ + protected TrieEntry lowerEntry(K key) { + // Basically: + // Follow the steps of adding an entry, but instead... + // + // - If we ever encounter a situation where we found an equal + // key, we return it's previousEntry immediately. + // + // - If we hit root (empty or not), return null. + // + // - If we have to add a new item, we temporarily add it, + // find the previousEntry to it, then remove the added item. + // + // These steps ensure that the returned value is always just before + // the key or null (if there was nothing before it). + + // TODO: Cleanup so that we don't actually have to add/remove from the + // tree. (We do it here because there are other well-defined + // functions to perform the search.) + int keyLength = length(key); + + if (keyLength == 0) { + return null; // there can never be anything before root. + } + + TrieEntry found = getNearestEntryForKey(key, keyLength); + if (key.equals(found.key)) + return previousEntry(found); + + int bitIndex = bitIndex(key, found.key); + if (isValidBitIndex(bitIndex)) { + TrieEntry added = new TrieEntry(key, null, bitIndex); + addEntry(added, keyLength); + incrementSize(); // must increment because remove will decrement + TrieEntry prior = previousEntry(added); + removeEntry(added); + modCount -= 2; // we didn't really modify it. + return prior; + } else if (isNullBitKey(bitIndex)) { + return null; + } else if (isEqualBitKey(bitIndex)) { + return previousEntry(found); + } + + // we should have exited above. + throw new IllegalStateException("invalid lookup: " + key); + } + + /** + * Returns a key-value mapping associated with the greatest key + * less than or equal to the given key, or null if there is no such key. + */ + protected TrieEntry floorEntry(K key) { + // TODO: Cleanup so that we don't actually have to add/remove from the + // tree. (We do it here because there are other well-defined + // functions to perform the search.) + int keyLength = length(key); + + if (keyLength == 0) { + if(!root.isEmpty()) + return root; + else + return null; + } + + TrieEntry found = getNearestEntryForKey(key, keyLength); + if (key.equals(found.key)) + return found; + + int bitIndex = bitIndex(key, found.key); + if (isValidBitIndex(bitIndex)) { + TrieEntry added = new TrieEntry(key, null, bitIndex); + addEntry(added, keyLength); + incrementSize(); // must increment because remove will decrement + TrieEntry floor = previousEntry(added); + removeEntry(added); + modCount -= 2; // we didn't really modify it. + return floor; + } else if (isNullBitKey(bitIndex)) { + if (!root.isEmpty()) + return root; + else + return null; + } else if (isEqualBitKey(bitIndex)) { + return found; + } + + // we should have exited above. + throw new IllegalStateException("invalid lookup: " + key); + } + + + /** + * Finds the subtree that contains the prefix. + * + * This is very similar to getR but with the difference that + * we stop the lookup if h.bitIndex > keyLength. + */ + private TrieEntry subtree(K prefix, int offset, int length) { + TrieEntry current = root.left; + TrieEntry path = root; + while(true) { + if(current.bitIndex <= path.bitIndex || length < current.bitIndex) + break; + + path = current; + if(!isBitSet(prefix, length+offset, current.bitIndex+offset)) { + current = current.left; + } else { + current = current.right; + } + } + + // Make sure the entry is valid for a subtree. + TrieEntry entry = current.isEmpty() ? path : current; + + // If entry is root, it can't be empty. + if (entry.isEmpty()) + return null; + + int offsetLength = offset + length; + + // if root && length of root is less than length of lookup, + // there's nothing. + // (this prevents returning the whole subtree if root has an empty + // string and we want to lookup things with "\0") + if(entry == root && length(entry.getKey()) < offsetLength) + return null; + + // Found key's length-th bit differs from our key + // which means it cannot be the prefix... + if(isBitSet(prefix, offsetLength, offsetLength) != + isBitSet(entry.key, length(entry.key), length)) { + return null; + } + + // ... or there are less than 'length' equal bits + int bitIndex = keyAnalyzer.bitIndex(prefix, offset, length, + entry.key, 0, length(entry.getKey())); + if (bitIndex >= 0 && bitIndex < length) + return null; + + return entry; + } + + /** A submap used for prefix views over the Trie. */ + private class PrefixSubMap extends SubMap { + protected final K prefix; + protected final int offset; + protected final int length; + private transient int keyModCount = 0; + protected int size; + + PrefixSubMap(K prefix, int offset, int length) { + this.prefix = prefix; + this.offset = offset; + this.length = length; + fromInclusive = false; + } + + public K firstKey() { + fixup(); + TrieEntry e; + if(fromKey == null) { + e = firstEntry(); + } else { + e = higherEntry(fromKey); + } + + K first = e != null ? e.getKey() : null; + if (e == null || !keyAnalyzer.isPrefix(prefix, offset, length, first)) + throw new NoSuchElementException(); + return first; + } + + public K lastKey() { + fixup(); + TrieEntry e; + if(toKey == null) { + e = lastEntry(); + } else { + e = lowerEntry(toKey); + } + + K last = e != null ? e.getKey() : null; + if (e == null || !keyAnalyzer.isPrefix(prefix, offset, length, last)) + throw new NoSuchElementException(); + return last; + } + + protected boolean inRange(K key) { + return keyAnalyzer.isPrefix(prefix, offset, length, key); + } + + protected boolean inRange2(K key) { + return keyAnalyzer.isPrefix(prefix, offset, length, key); + } + + protected boolean inToRange(K key, boolean forceInclusive) { + return keyAnalyzer.isPrefix(prefix, offset, length, key); + } + + protected boolean inFromRange(K key, boolean forceInclusive) { + return keyAnalyzer.isPrefix(prefix, offset, length, key); + } + + private void fixup() { + // The trie has changed since we last + // found our toKey / fromKey + if(modCount != keyModCount) { + Iterator> iter = entrySet().iterator(); + size = 0; + + Map.Entry entry = null; + if(iter.hasNext()) { + entry = iter.next(); + size = 1; + } + + fromKey = entry == null ? null : entry.getKey(); + if(fromKey != null) { + TrieEntry prior = previousEntry((TrieEntry)entry); + fromKey = prior == null ? null : prior.getKey(); + } + + toKey = fromKey; + + while(iter.hasNext()) { + size++; + entry = iter.next(); + } + + toKey = entry == null ? null : entry.getKey(); + + if(toKey != null) { + entry = nextEntry((TrieEntry)entry); + toKey = entry == null ? null : entry.getKey(); + } + + keyModCount = modCount; + } + } + + protected Set> newSubMapEntrySet() { + return new PrefixEntrySetView(); + } + + private class PrefixEntrySetView extends SubMap.EntrySetView { + private TrieEntry prefixStart; + private int iterModCount = 0; + + public int size() { + fixup(); + return PrefixSubMap.this.size; + } + + @SuppressWarnings("unchecked") + public Iterator> iterator() { + if(modCount != iterModCount) { + prefixStart = subtree(prefix, offset, length); + iterModCount = modCount; + } + + if(prefixStart == null) { + return new EmptyIterator(); + } else if(length >= prefixStart.bitIndex){ + return new SingletonIterator(prefixStart); + } else { + return new PrefixEntryIterator(prefixStart, prefix, offset, length); + } + } + } + } + + private class SubMap extends AbstractMap implements SortedMap, java.io.Serializable { + + // TODO: add serialVersionUID + + /** The key to start from, null if the beginning. */ + protected K fromKey; + + /** The key to end at, null if till the end. */ + protected K toKey; + + /** Whether or not the 'from' is inclusive. */ + protected boolean fromInclusive; + + /** Whether or not the 'to' is inclusive. */ + protected boolean toInclusive; + + /** + * Constructs a blank SubMap -- this should ONLY be used + * by subclasses that wish to lazily construct their + * fromKey or toKey + */ + protected SubMap() {} + + SubMap(K fromKey, K toKey) { + if(fromKey == null && toKey == null) + throw new IllegalArgumentException("must have a from or to!"); + if(fromKey != null && toKey != null && keyAnalyzer.compare(fromKey, toKey) > 0) + throw new IllegalArgumentException("fromKey > toKey"); + this.fromKey = fromKey; + this.toKey = toKey; + fromInclusive = true; + } + + public boolean isEmpty() { + return entrySet().isEmpty(); + } + + @SuppressWarnings("unchecked") + public boolean containsKey(Object key) { + return inRange((K) key) && PatriciaTrie.this.containsKey(key); + } + + @SuppressWarnings("unchecked") + public V remove(Object key) { + if(!inRange((K)key)) + return null; + return PatriciaTrie.this.remove(key); + } + + @SuppressWarnings("unchecked") + public V get(Object key) { + if (!inRange((K) key)) + return null; + return PatriciaTrie.this.get(key); + } + + public V put(K key, V value) { + if (!inRange(key)) + throw new IllegalArgumentException("key out of range"); + return PatriciaTrie.this.put(key, value); + } + + public Comparator comparator() { + return keyAnalyzer; + } + + public K firstKey() { + TrieEntry e; + if(fromKey == null) { + e = firstEntry(); + } else { + if(fromInclusive) + e = ceilingEntry(fromKey); + else + e = higherEntry(fromKey); + } + + K first = e != null ? e.getKey() : null; + if (e == null || toKey != null && !inToRange(first, false)) + throw new NoSuchElementException(); + return first; + } + + public K lastKey() { + TrieEntry e; + if(toKey == null) { + e = lastEntry(); + } else { + if(toInclusive) + e = floorEntry(toKey); + else + e = lowerEntry(toKey); + } + + K last = e != null ? e.getKey() : null; + if (e == null || fromKey != null && !inFromRange(last, false)) + throw new NoSuchElementException(); + return last; + } + + private transient Set> entrySet; + + public Set> entrySet() { + if(entrySet == null) + entrySet = newSubMapEntrySet(); + return entrySet; + } + + protected Set> newSubMapEntrySet() { + return new EntrySetView(); + } + + class EntrySetView extends AbstractSet> { + private transient int size = -1, sizeModCount; + + public int size() { + if (size == -1 || sizeModCount != PatriciaTrie.this.modCount) { + size = 0; sizeModCount = PatriciaTrie.this.modCount; + Iterator i = iterator(); + while (i.hasNext()) { + size++; + i.next(); + } + } + return size; + } + + public boolean isEmpty() { + return !iterator().hasNext(); + } + + @SuppressWarnings("unchecked") + public boolean contains(Object o) { + if (!(o instanceof Map.Entry)) + return false; + Map.Entry entry = (Map.Entry) o; + K key = entry.getKey(); + if (!inRange(key)) + return false; + TrieEntry node = getEntry(key); + return node != null && + valEquals(node.getValue(), entry.getValue()); + } + + @SuppressWarnings("unchecked") + public boolean remove(Object o) { + if (!(o instanceof Map.Entry)) + return false; + Map.Entry entry = (Map.Entry) o; + K key = entry.getKey(); + if (!inRange(key)) + return false; + TrieEntry node = getEntry(key); + if (node!=null && valEquals(node.getValue(),entry.getValue())){ + removeEntry(node); + return true; + } + return false; + } + + public Iterator> iterator() { + return new SubMapEntryIterator( + (fromKey == null ? firstEntry() : ceilingEntry(fromKey)), + (toKey == null ? null : ceilingEntry(toKey))); + } + } + + public SortedMap subMap(K fromKey, K toKey) { + if (!inRange2(fromKey)) + throw new IllegalArgumentException("fromKey out of range"); + if (!inRange2(toKey)) + throw new IllegalArgumentException("toKey out of range"); + return new SubMap(fromKey, toKey); + } + + public SortedMap headMap(K toKey) { + if (!inRange2(toKey)) + throw new IllegalArgumentException("toKey out of range"); + return new SubMap(fromKey, toKey); + } + + public SortedMap tailMap(K fromKey) { + if (!inRange2(fromKey)) + throw new IllegalArgumentException("fromKey out of range"); + return new SubMap(fromKey, toKey); + } + + protected boolean inRange(K key) { + return (fromKey == null || inFromRange(key, false)) && + (toKey == null || inToRange(key, false)); + } + + // This form allows the high endpoint (as well as all legit keys) + protected boolean inRange2(K key) { + return (fromKey == null || inFromRange(key, false)) && + (toKey == null || inToRange(key, true)); + } + + protected boolean inToRange(K key, boolean forceInclusive) { + int ret = keyAnalyzer.compare(key, toKey); + if(toInclusive || forceInclusive) + return ret <= 0; + else + return ret < 0; + } + + protected boolean inFromRange(K key, boolean forceInclusive) { + int ret = keyAnalyzer.compare(key, fromKey); + if (fromInclusive || forceInclusive) + return ret >= 0; + else + return ret > 0; + } + } + + private static class EmptyIterator implements Iterator { + public boolean hasNext() { + return false; + } + + public Object next() { + throw new NoSuchElementException(); + } + + public void remove() { + throw new IllegalStateException(); + } + + } +} + diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/trie/Trie.java b/plugins/spellchecker/src/com/intellij/spellchecker/trie/Trie.java new file mode 100644 index 000000000000..49aadb369124 --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/trie/Trie.java @@ -0,0 +1,172 @@ +/* + * Copyright 2000-2009 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.intellij.spellchecker.trie; + + +import java.util.Map; +import java.util.SortedMap; + +/** + * Defines the interface for a prefix tree, an ordered tree data structure. For + * more information, see Tries. + * + * @author Roger Kapsi + * @author Sam Berlin + */ +public interface Trie extends SortedMap { + + /** + * Returns a view of this Trie of all elements that are + * prefixed by the given key. + *

+ * In a fixed-keysize Trie, this is essentially a 'get' operation. + *

+ * For example, if the Trie contains 'Lime', 'LimeWire', + * 'LimeRadio', 'Lax', 'Later', 'Lake', and 'Lovely', then + * a lookup of 'Lime' would return 'Lime', 'LimeRadio', and 'LimeWire'. + */ + public SortedMap getPrefixedBy(K key); + + /** + * Returns a view of this Trie of all elements that are + * prefixed by the length of the key. + *

+ * Fixed-keysize Tries will not support this operation + * (because all keys will be the same length). + *

+ * For example, if the Trie contains 'Lime', 'LimeWire', + * 'LimeRadio', 'Lax', 'Later', 'Lake', and 'Lovely', then + * a lookup of 'LimePlastics' with a length of 4 would + * return 'Lime', 'LimeRadio', and 'LimeWire'. + */ + public SortedMap getPrefixedBy(K key, int length); + + /** + * Returns a view of this Trie of all elements that are prefixed + * by the key, starting at the given offset and for the given length. + *

+ * Fixed-keysize Tries will not support this operation + * (because all keys are the same length). + *

+ * For example, if the Trie contains 'Lime', 'LimeWire', + * 'LimeRadio', 'Lax', 'Later', 'Lake', and 'Lovely', then + * a lookup of 'The Lime Plastics' with an offset of 4 and a + * length of 4 would return 'Lime', 'LimeRadio', and 'LimeWire'. + */ + public SortedMap getPrefixedBy(K key, int offset, int length); + + /** + * Returns a view of this Trie of all elements that are prefixed + * by the number of bits in the given Key. + *

+ * Fixed-keysize Tries can support this operation as a way to do + * lookups of partial keys. That is, if the Trie is storing IP + * addresses, you can lookup all addresses that begin with + * '192.168' by providing the key '192.168.X.X' and a length of 16 + * would return all addresses that begin with '192.168'. + */ + public SortedMap getPrefixedByBits(K key, int bitLength); + + /** + * Returns the value for the entry whose key is closest in a bitwise + * XOR metric to the given key. This is NOT lexicographic closeness. + * For example, given the keys:
+ * D = 1000100
+ * H = 1001000
+ * L = 1001100
+ *

+ * If the Trie contained 'H' and 'L', a lookup of 'D' would return 'L', + * because the XOR distance between D & L is smaller than the XOR distance + * between D & H. + */ + public V select(K key); + + /** + * Iterates through the Trie, starting with the entry whose bitwise + * value is closest in an XOR metric to the given key. After the closest + * entry is found, the Trie will call select on that entry and continue + * calling select for each entry (traversing in order of XOR closeness, + * NOT lexicographically) until the cursor returns + * Cursor.SelectStatus.EXIT.
+ * The cursor can return Cursor.SelectStatus.CONTINUE to + * continue traversing.
+ * Cursor.SelectStatus.REMOVE_AND_EXIT is used to remove the current element + * and stop traversing. + *

+ * Note: The {@link Cursor.SelectStatus#REMOVE} operation is not supported. + * + * @return The entry the cursor returned EXIT on, or null if it continued + * till the end. + */ + public Map.Entry select(K key, Cursor cursor); + + /** + * Traverses the Trie in lexicographical order. Cursor.select + * will be called on each entry.

+ * The traversal will stop when the cursor returns Cursor.SelectStatus.EXIT.
+ * Cursor.SelectStatus.CONTINUE is used to continue traversing.
+ * Cursor.SelectStatus.REMOVE is used to remove the element that was + * selected and continue traversing.
+ * Cursor.SelectStatus.REMOVE_AND_EXIT is used to remove the current element + * and stop traversing. + * + * @return The entry the cursor returned EXIT on, or null if it continued + * till the end. + */ + public Map.Entry traverse(Cursor cursor); + + /** + * An interface used by a {@link Trie}. A {@link Trie} selects items by + * closeness and passes the items to the Cursor. You can then + * decide what to do with the key-value pair and the return value + * from {@link #select(java.util.Map.Entry)} tells the Trie + * what to do next. + *

+ * Cursor returns status/selection status might be: + * + * + * + * + * + * + *
Return ValueStatus
EXITFinish the Trie operation
CONTINUELook at the next element in the traversal
REMOVE_AND_EXITRemove the entry and stop iterating
REMOVERemove the entry and continue iterating
+ + * Note: {@link Trie#select(Object, org.limewire.collection.Trie.Cursor)} does + * not support REMOVE. + * + * @param Key Type + * @param Key Value + */ + public static interface Cursor { + + /** + * Notification that the Trie is currently looking at the given entry. + * Return EXIT to finish the Trie operation, + * CONTINUE to look at the next entry, REMOVE + * to remove the entry and continue iterating, or + * REMOVE_AND_EXIT to remove the entry and stop iterating. + * Not all operations support REMOVE. + * + */ + public SelectStatus select(Map.Entry entry); + + /** The mode during selection. */ + public static enum SelectStatus { + EXIT, CONTINUE, REMOVE, REMOVE_AND_EXIT; + } + } +} + diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/util/SpellCheckerBundle.java b/plugins/spellchecker/src/com/intellij/spellchecker/util/SpellCheckerBundle.java index 307e51c19038..4ecb76209c44 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/util/SpellCheckerBundle.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/util/SpellCheckerBundle.java @@ -1,36 +1,37 @@ -/* - * Copyright 2000-2009 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.intellij.spellchecker.util; - -import com.intellij.CommonBundle; -import org.jetbrains.annotations.NonNls; -import org.jetbrains.annotations.PropertyKey; - -import java.util.ResourceBundle; - - -public final class SpellCheckerBundle { - @NonNls - private static final String BUNDLE_NAME = "com.intellij.spellchecker.util.SpellCheckerBundle"; - private static final ResourceBundle BUNDLE = ResourceBundle.getBundle(BUNDLE_NAME); - - private SpellCheckerBundle() { - } - - public static String message(@PropertyKey(resourceBundle = BUNDLE_NAME) String key, Object... params) { - return CommonBundle.message(BUNDLE, key, params); - } -} +/* + * Copyright 2000-2009 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.intellij.spellchecker.util; + +import com.intellij.CommonBundle; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.PropertyKey; + +import java.util.ResourceBundle; + + +public final class SpellCheckerBundle { + @NonNls + private static final String BUNDLE_NAME = "com.intellij.spellchecker.util.SpellCheckerBundle"; + private static final ResourceBundle BUNDLE = ResourceBundle.getBundle(BUNDLE_NAME); + + private SpellCheckerBundle() { + } + + public static String message(@PropertyKey(resourceBundle = BUNDLE_NAME) String key, Object... params) { + return CommonBundle.message(BUNDLE, key, params); + + } +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/util/SpellCheckerBundle.properties b/plugins/spellchecker/src/com/intellij/spellchecker/util/SpellCheckerBundle.properties index f751b4a008f2..fc34af4e389a 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/util/SpellCheckerBundle.properties +++ b/plugins/spellchecker/src/com/intellij/spellchecker/util/SpellCheckerBundle.properties @@ -1,28 +1,27 @@ -change.to.0=Change to ''{0}'' -change.to=Change to... -rename.to.0=Rename to ''{0}'' -rename.to=Rename to... -spelling=Spelling -spellchecking.inspection.name=Typo -comments.with.mistakes=Comments with mistakes -word.0.1.is.misspelled=Word ''{0}'' is misspelled -add.0.to.dictionary=Accept ''{0}'' as correct -class.name.with.mistakes=Class name with mistakes -method.name.with.mistakes=Method name with mistakes -field.name.with.mistakes=Field name with mistakes -xml.with.mistakes=XML with mistakes -property.value.with.mistakes=Property value with mistakes -string.value.with.mistakes=Literal expression with mistakes -local.variable.name.with.mistakes=Local variable name with mistakes -doccomment.with.mistakes=Doc comment with mistake -user.dictionary=User Dictionary -ignored.words=Ignored Words -enter.simple.word=Enter simple word: -add.new.word=Add new word -entered.word.0.is.mixed.cased.you.must.enter.simple.word=Entered word {0} is mixed cased. You must enter simple word -entered.word.0.is.correct.you.no.need.to.add.this.in.list=Entered word {0} is correct. You no need to add this in list. -no.suggestions= -process.code=Process code -process.literals=Process literals -process.comments=Process comments - +change.to.0=Typo: Change to ''{0}'' +change.to=Typo: Change to... +rename.to.0=Typo: Rename to ''{0}'' +rename.to=Typo: Rename to... +spelling=Spelling +spellchecking.inspection.name=Typo +comments.with.mistakes=Comments with mistakes +word.0.1.is.misspelled=Typo: In word ''{0}'' +add.0.to.dictionary=Typo: Accept ''{0}'' as correct +class.name.with.mistakes=Class name with mistakes +method.name.with.mistakes=Method name with mistakes +field.name.with.mistakes=Field name with mistakes +xml.with.mistakes=XML with mistakes +property.value.with.mistakes=Property value with mistakes +string.value.with.mistakes=Literal expression with mistakes +local.variable.name.with.mistakes=Local variable name with mistakes +doccomment.with.mistakes=Doc comment with mistake +user.dictionary=User Dictionary +ignored.words=Ignored Words +enter.simple.word=Enter simple word: +add.new.word=Add new word +entered.word.0.is.mixed.cased.you.must.enter.simple.word=Entered word {0} is mixed cased. You must enter simple word +entered.word.0.is.correct.you.no.need.to.add.this.in.list=Entered word {0} is correct. You no need to add this in list. +no.suggestions= +process.code=Process code +process.literals=Process literals +process.comments=Process comments diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/util/Strings.java b/plugins/spellchecker/src/com/intellij/spellchecker/util/Strings.java index 306ff3d17b5f..675c608b6925 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/util/Strings.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/util/Strings.java @@ -1,76 +1,76 @@ -/* - * Copyright 2000-2009 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.intellij.spellchecker.util; - -import java.util.List; - -/** - * Text utility. - */ -public final class Strings { - private Strings() { - } - - public static boolean isCapitalized(String word) { - if (word.length() == 0) return false; - - boolean lowCase = true; - for (int i = 1; i < word.length() && lowCase; i++) { - lowCase = Character.isLowerCase(word.charAt(i)); - } - - return Character.isUpperCase(word.charAt(0)) && lowCase; - } - - public static boolean isUpperCase(String word) { - boolean upperCase = true; - for (int i = 0; i < word.length() && upperCase; i++) { - upperCase = Character.isUpperCase(word.charAt(i)); - } - - return upperCase; - } - - public static boolean isMixedCase(String word) { - if (word.length() < 2) return false; - - String tail = word.substring(1); - String lowerCase = tail.toLowerCase(); - return !tail.equals(lowerCase) && !isUpperCase(word); - } - - public static String capitalize(String word) { - if (word.length() == 0) return word; - - StringBuffer buf = new StringBuffer(word); - buf.setCharAt(0, Character.toUpperCase(buf.charAt(0))); - return buf.toString(); - } - - public static void capitalize(List words) { - for (int i = 0; i < words.size(); i++) { - words.set(i, capitalize(words.get(i))); - } - } - - public static void upperCase(List words) { - for (int i = 0; i < words.size(); i++) { - words.set(i, words.get(i).toUpperCase()); - } - } - - -} +/* + * Copyright 2000-2009 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.intellij.spellchecker.util; + +import java.util.List; + +/** + * Text utility. + */ +public final class Strings { + private Strings() { + } + + public static boolean isCapitalized(String word) { + if (word.length() == 0) return false; + + boolean lowCase = true; + for (int i = 1; i < word.length() && lowCase; i++) { + lowCase = Character.isLowerCase(word.charAt(i)); + } + + return Character.isUpperCase(word.charAt(0)) && lowCase; + } + + public static boolean isUpperCase(String word) { + boolean upperCase = true; + for (int i = 0; i < word.length() && upperCase; i++) { + upperCase = Character.isUpperCase(word.charAt(i)); + } + + return upperCase; + } + + public static boolean isMixedCase(String word) { + if (word.length() < 2) return false; + + String tail = word.substring(1); + String lowerCase = tail.toLowerCase(); + return !tail.equals(lowerCase) && !isUpperCase(word); + } + + public static String capitalize(String word) { + if (word.length() == 0) return word; + + StringBuffer buf = new StringBuffer(word); + buf.setCharAt(0, Character.toUpperCase(buf.charAt(0))); + return buf.toString(); + } + + public static void capitalize(List words) { + for (int i = 0; i < words.size(); i++) { + words.set(i, capitalize(words.get(i))); + } + } + + public static void upperCase(List words) { + for (int i = 0; i < words.size(); i++) { + words.set(i, words.get(i).toUpperCase()); + } + } + + +} diff --git a/plugins/spellchecker/testData/inspection/classNameWithMistakes/TestUpgade.java b/plugins/spellchecker/testData/inspection/classNameWithMistakes/TestUpgade.java index 8625f8f02c90..94bdaf78f9f1 100644 --- a/plugins/spellchecker/testData/inspection/classNameWithMistakes/TestUpgade.java +++ b/plugins/spellchecker/testData/inspection/classNameWithMistakes/TestUpgade.java @@ -15,5 +15,5 @@ */ package testData.inspection.classNameWithMistakes; -class TestUpgade { +class TestUpgade { } diff --git a/plugins/spellchecker/testData/inspection/commentsWithMistakes/A.xml b/plugins/spellchecker/testData/inspection/commentsWithMistakes/A.xml index eb2062d35943..fcc28ba38b59 100644 --- a/plugins/spellchecker/testData/inspection/commentsWithMistakes/A.xml +++ b/plugins/spellchecker/testData/inspection/commentsWithMistakes/A.xml @@ -1,6 +1,6 @@ - - tag value + + tag value plain text \ No newline at end of file diff --git a/plugins/spellchecker/testData/inspection/commentsWithMistakes/SPITest1.java b/plugins/spellchecker/testData/inspection/commentsWithMistakes/SPITest1.java index c94fc301ab8b..2b4d1c9cc593 100644 --- a/plugins/spellchecker/testData/inspection/commentsWithMistakes/SPITest1.java +++ b/plugins/spellchecker/testData/inspection/commentsWithMistakes/SPITest1.java @@ -21,13 +21,13 @@ package testData.inspection.commentsWithMistakes.data.java.src; */ class SPITest1 { /* boolean is Java keyword - commment + commment */ - // single line upgade + // single line upgade void method() { /* - werty within method + werty within method */ - // single line newss within method + // single line newss within method } } diff --git a/plugins/spellchecker/testData/inspection/commentsWithMistakes/test.html b/plugins/spellchecker/testData/inspection/commentsWithMistakes/test.html index 0405dd090da1..cd210a55603b 100644 --- a/plugins/spellchecker/testData/inspection/commentsWithMistakes/test.html +++ b/plugins/spellchecker/testData/inspection/commentsWithMistakes/test.html @@ -1,5 +1,5 @@ -

+

\ No newline at end of file diff --git a/plugins/spellchecker/testData/inspection/commentsWithMistakes/test.txt b/plugins/spellchecker/testData/inspection/commentsWithMistakes/test.txt index 74752f5027f3..0f2937c30b4b 100644 --- a/plugins/spellchecker/testData/inspection/commentsWithMistakes/test.txt +++ b/plugins/spellchecker/testData/inspection/commentsWithMistakes/test.txt @@ -1 +1 @@ -simple ttest file (just plain text) \ No newline at end of file +simple ttest file (just plain text) \ No newline at end of file diff --git a/plugins/spellchecker/testData/inspection/docCommentWithMistakes/SPITest6.java b/plugins/spellchecker/testData/inspection/docCommentWithMistakes/SPITest6.java index 59875179cf29..0cc7177b26fe 100644 --- a/plugins/spellchecker/testData/inspection/docCommentWithMistakes/SPITest6.java +++ b/plugins/spellchecker/testData/inspection/docCommentWithMistakes/SPITest6.java @@ -16,7 +16,7 @@ package testData.inspection.docCommentWithMistakes.data.java.src; /** - * doc commment + * doc commment * * @author Test Test */ diff --git a/plugins/spellchecker/testData/inspection/fieldNameWithMistakes/SPITest2.java b/plugins/spellchecker/testData/inspection/fieldNameWithMistakes/SPITest2.java index 69f9bc531f86..f9fce20dc048 100644 --- a/plugins/spellchecker/testData/inspection/fieldNameWithMistakes/SPITest2.java +++ b/plugins/spellchecker/testData/inspection/fieldNameWithMistakes/SPITest2.java @@ -16,7 +16,7 @@ package testData.inspection.fieldNameWithMistakes.data.java.src; class SPITest2 { - private static final String TEST_CONASTANT = "Test Constant Value"; - private String ttest; - private String camelCaseTtest; + private static final String TEST_CONASTANT = "Test Constant Value"; + private String ttest; + private String camelCaseTtest; } diff --git a/plugins/spellchecker/testData/inspection/groovy/Test.groovy b/plugins/spellchecker/testData/inspection/groovy/Test.groovy index 6e603abfbc93..c35a6c90fc75 100644 --- a/plugins/spellchecker/testData/inspection/groovy/Test.groovy +++ b/plugins/spellchecker/testData/inspection/groovy/Test.groovy @@ -1,2 +1,2 @@ -def abc(String dddd) { +def abc(String dddd) { } diff --git a/plugins/spellchecker/testData/inspection/js/test.js b/plugins/spellchecker/testData/inspection/js/test.js index ec237b1fa328..5c0f3b3d8c91 100644 --- a/plugins/spellchecker/testData/inspection/js/test.js +++ b/plugins/spellchecker/testData/inspection/js/test.js @@ -14,14 +14,14 @@ * limitations under the License. */ -/*some commments */ -function testFuunction(){ - var upddate = "test variable"; +/*some commments */ +function testFuunction(){ + var upddate = "test variable"; } var obj = { }; -obj.ttest = function(){ +obj.ttest = function(){ }; \ No newline at end of file diff --git a/plugins/spellchecker/testData/inspection/localVariableNameWithMistakes/SPITest3.java b/plugins/spellchecker/testData/inspection/localVariableNameWithMistakes/SPITest3.java index 2cad16df18d4..1310c707a983 100644 --- a/plugins/spellchecker/testData/inspection/localVariableNameWithMistakes/SPITest3.java +++ b/plugins/spellchecker/testData/inspection/localVariableNameWithMistakes/SPITest3.java @@ -17,7 +17,7 @@ package testData.inspection.localVariableNameWithMistakes.data.java.src; class SPITest3 { public void method() { - String camelCaseTtest = "she is reading"; - String ttest = "she is reading"; + String camelCaseTtest = "she is reading"; + String ttest = "she is reading"; } } diff --git a/plugins/spellchecker/testData/inspection/methodNameWithMistakes/SPITest4.java b/plugins/spellchecker/testData/inspection/methodNameWithMistakes/SPITest4.java index 0dbc4570fe52..fe532e4f84a0 100644 --- a/plugins/spellchecker/testData/inspection/methodNameWithMistakes/SPITest4.java +++ b/plugins/spellchecker/testData/inspection/methodNameWithMistakes/SPITest4.java @@ -16,8 +16,8 @@ package testData.inspection.methodNameWithMistakes.data.java.src; class SPITest4 { - public void methodTtestWithMistake() { + public void methodTtestWithMistake() { } - public void methad() { + public void methad() { } } diff --git a/plugins/spellchecker/testData/inspection/php/test.php b/plugins/spellchecker/testData/inspection/php/test.php index e6a783f1b8eb..f9cf9ab916b1 100644 --- a/plugins/spellchecker/testData/inspection/php/test.php +++ b/plugins/spellchecker/testData/inspection/php/test.php @@ -1,5 +1,5 @@ Classs*/ TestClasss { +class /*TestClasss*/ TestClasss { } ?> \ No newline at end of file diff --git a/plugins/spellchecker/testData/inspection/stringWithMistakes/SPITest5.java b/plugins/spellchecker/testData/inspection/stringWithMistakes/SPITest5.java index 58c6264b7999..9c6ba502c87c 100644 --- a/plugins/spellchecker/testData/inspection/stringWithMistakes/SPITest5.java +++ b/plugins/spellchecker/testData/inspection/stringWithMistakes/SPITest5.java @@ -16,5 +16,5 @@ package testData.inspection.stringWithMistakes.data.java.src; class SPITest5 { - public final static String test = "upgrdae"; + public final static String test = "upgrdae"; } diff --git a/plugins/spellchecker/testData/inspection/xmlWithMistakes/test.jsp b/plugins/spellchecker/testData/inspection/xmlWithMistakes/test.jsp index bedf628bc5e9..45a958f73c20 100644 --- a/plugins/spellchecker/testData/inspection/xmlWithMistakes/test.jsp +++ b/plugins/spellchecker/testData/inspection/xmlWithMistakes/test.jsp @@ -17,10 +17,10 @@ --%> - Test <TYPO descr="Word 'ttitle' is misspelled">ttitle</TYPO> + Test <TYPO descr="Typo: In word 'ttitle'">ttitle</TYPO> -
ttitle">Test ccontent goes here
+
ttitle">Test ccontent goes here
test link \ No newline at end of file diff --git a/plugins/spellchecker/testData/inspection/xmlWithMistakes/test.xml b/plugins/spellchecker/testData/inspection/xmlWithMistakes/test.xml index ce57a68e25c4..28192c0fde34 100644 --- a/plugins/spellchecker/testData/inspection/xmlWithMistakes/test.xml +++ b/plugins/spellchecker/testData/inspection/xmlWithMistakes/test.xml @@ -1,3 +1,3 @@ -ttest attribute"> - plain text gooes here +ttest attribute"> + plain text gooes here \ No newline at end of file diff --git a/plugins/spellchecker/testSrc/com/intellij/spellchecker/inspector/SplitterTest.java b/plugins/spellchecker/testSrc/com/intellij/spellchecker/inspector/SplitterTest.java index 1e339099fd77..57843e55cf9d 100644 --- a/plugins/spellchecker/testSrc/com/intellij/spellchecker/inspector/SplitterTest.java +++ b/plugins/spellchecker/testSrc/com/intellij/spellchecker/inspector/SplitterTest.java @@ -15,8 +15,8 @@ */ package com.intellij.spellchecker.inspector; -import com.intellij.spellchecker.CheckArea; -import com.intellij.spellchecker.TextSplitter; +import com.intellij.spellchecker.inspections.CheckArea; +import com.intellij.spellchecker.inspections.TextSplitter; import junit.framework.Assert; import junit.framework.TestCase; import org.jetbrains.annotations.NotNull; @@ -80,7 +80,20 @@ public class SplitterTest extends TestCase { correctIgnored(checkAreas, text, new String[]{}); } - + + public void testWordWithApostrophe4() { + String text = "we'll"; + List checkAreas = TextSplitter.splitText(text); + correctListToCheck(checkAreas, text, new String[]{"we'll"}); + correctIgnored(checkAreas, text, new String[]{}); + } + + public void testWordWithApostrophe5() { + String text = "I'm you're we'll"; + List checkAreas = TextSplitter.splitText(text); + correctListToCheck(checkAreas, text, new String[]{"you're","we'll"}); + correctIgnored(checkAreas, text, new String[]{"I'm"}); + } public void testConstantName() { String text = "TEST_CONSTANT";