Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
4817799
Implement persistent cache for TypoDiagnostic with CheckedWordsHolder
Copilot Oct 23, 2025
2bfb4bb
Fix logging level and test robustness for CheckedWordsHolder
Copilot Oct 23, 2025
44e01bb
Refactor CheckedWordsHolder to use WordStatus enum and let Spring Cac…
Copilot Oct 23, 2025
8d9b280
Add persistent disk cache with EhCache + Caffeine composite cache
Copilot Oct 23, 2025
1e26cd5
Refactor cache configuration: separate EhCache for typoCache, program…
Copilot Oct 23, 2025
82b0f86
Add .bsl-ls-cache/ to .gitignore
Copilot Oct 23, 2025
323c16c
Remove singleton pattern from EhCache manager, use proper Spring bean…
Copilot Oct 23, 2025
aac548e
Use Path.of(".") instead of System.getProperty("user.dir") for cache …
Copilot Oct 23, 2025
e8665c8
Address review feedback: increase heap size, improve test order, opti…
Copilot Oct 23, 2025
9f5cd94
Fix code style: add proper imports and revert unnecessary caffeine co…
Copilot Oct 23, 2025
889a4b9
Revert to original check logic pattern with putIfAbsent semantics
Copilot Oct 23, 2025
c5d516a
Remove putWordStatusIfAbsent method, use MISSING filter with putWordS…
Copilot Oct 25, 2025
bfef0d3
Fix test failures: unique cache directories per context and suppress …
Copilot Oct 25, 2025
a438ecf
Move EhCache logging config to application.properties and restore per…
Copilot Oct 25, 2025
52800ab
Fix test failures: use unique cache directories per Spring context an…
Copilot Oct 25, 2025
b54a640
Move cache directory configuration to application.properties, remove …
Copilot Oct 25, 2025
1c39d49
Rename cache property from bsl.typo.cache.dir to app.cache.path
Copilot Oct 25, 2025
5a5aabe
Add automatic cleanup of test cache directories after test run
Copilot Oct 25, 2025
d63d4fe
Fix compilation error and unchecked cast warning in CacheConfiguration
Copilot Oct 25, 2025
6804b8a
Add proper import for ValueRetrievalException
Copilot Oct 25, 2025
fdce260
Translate JavaDoc to Russian for consistency
Copilot Oct 25, 2025
158cb9f
Remove javax.cache:cache-api dependency
nixel2007 Oct 28, 2025
b7ac4f1
Merge branch 'develop' into copilot/implement-persistent-cache-typo-d…
nixel2007 Oct 28, 2025
2ce8ea2
Merge branch 'develop' into copilot/implement-persistent-cache-typo-d…
nixel2007 Nov 6, 2025
467cfbc
Extract EhCache wrapper into separate component with tests
Copilot Nov 6, 2025
28f2cc5
Refactor EhCacheAdapter initialization in tests
nixel2007 Nov 6, 2025
d36c235
Improve error handling in EhCacheAdapter tests
nixel2007 Nov 7, 2025
d9e745e
Translate comments and documentation in CheckedWordsHolder and WordSt…
nixel2007 Nov 7, 2025
104febc
Add persistent cache diagnostics section to Typo.md
nixel2007 Nov 7, 2025
3c93adf
Increase heap size in CacheConfiguration for improved performance
nixel2007 Nov 7, 2025
d8552f7
Fix QF
nixel2007 Nov 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,5 @@ bsl-language-server_*.zip
*.log
*.hprof
/.idea/material_theme_project_new.xml
__pycache__/
.bsl-ls-cache/
__pycache__/
9 changes: 9 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ dependencies {

// кэширование
api("com.github.ben-manes.caffeine", "caffeine", "3.2.0")
api("org.ehcache:ehcache:3.10.8")

// lsp4j core
api("org.eclipse.lsp4j", "org.eclipse.lsp4j", "0.24.0")
Expand Down Expand Up @@ -208,6 +209,14 @@ tasks.test {

val jmockitPath = classpath.find { it.name.contains("jmockit") }!!.absolutePath
jvmArgs("-javaagent:${jmockitPath}")

// Cleanup test cache directories after tests complete
doLast {
val tmpDir = File(System.getProperty("java.io.tmpdir"))
tmpDir.listFiles()?.filter { it.name.startsWith("bsl-ls-cache-") }?.forEach { cacheDir ->
cacheDir.deleteRecursively()
}
}
}

tasks.check {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,16 @@
import com.github._1c_syntax.bsl.languageserver.diagnostics.metadata.DiagnosticSeverity;
import com.github._1c_syntax.bsl.languageserver.diagnostics.metadata.DiagnosticTag;
import com.github._1c_syntax.bsl.languageserver.diagnostics.metadata.DiagnosticType;
import com.github._1c_syntax.bsl.languageserver.diagnostics.typo.CheckedWordsHolder;
import com.github._1c_syntax.bsl.languageserver.diagnostics.typo.JLanguageToolPool;
import com.github._1c_syntax.bsl.languageserver.diagnostics.typo.WordStatus;
import com.github._1c_syntax.bsl.languageserver.utils.Trees;
import com.github._1c_syntax.bsl.parser.BSLParser;
import com.github._1c_syntax.bsl.parser.BSLParserRuleContext;
import com.github._1c_syntax.utils.CaseInsensitivePattern;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.antlr.v4.runtime.Token;
import org.apache.commons.lang3.StringUtils;
Expand All @@ -49,7 +52,6 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
Expand All @@ -63,22 +65,17 @@
}
)
@Slf4j
@RequiredArgsConstructor
public class TypoDiagnostic extends AbstractDiagnostic {

private final CheckedWordsHolder checkedWordsHolder;

@Getter(lazy = true, value = AccessLevel.PRIVATE)
private static final Map<String, JLanguageToolPool> languageToolPoolMap = Map.of(
"en", new JLanguageToolPool(Languages.getLanguageForShortCode("en-US")),
"ru", new JLanguageToolPool(Languages.getLanguageForShortCode("ru"))
);

/**
* Карта, хранящая результат проверки слова (ошибка/нет ошибки) в разрезе языков.
*/
private static final Map<String, Map<String, Boolean>> checkedWords = Map.of(
"en", new ConcurrentHashMap<>(),
"ru", new ConcurrentHashMap<>()
);

private static final Pattern SPACES_PATTERN = Pattern.compile("\\s+");
private static final Pattern QUOTE_PATTERN = Pattern.compile("\"");
private static final String FORMAT_STRING_RU = "Л=|ЧЦ=|ЧДЦ=|ЧС=|ЧРД=|ЧРГ=|ЧН=|ЧВН=|ЧГ=|ЧО=|ДФ=|ДЛФ=|ДП=|БЛ=|БИ=";
Expand Down Expand Up @@ -166,12 +163,11 @@ private Map<String, List<Token>> getTokensMap(
protected void check() {

String lang = info.getResourceString("diagnosticLanguage");
Map<String, Boolean> checkedWordsForLang = checkedWords.get(lang);
Map<String, List<Token>> tokensMap = getTokensMap(documentContext);

// build string of unchecked words
Set<String> uncheckedWords = tokensMap.keySet().stream()
.filter(word -> !checkedWordsForLang.containsKey(word))
.filter(word -> checkedWordsHolder.getWordStatus(lang, word) == WordStatus.MISSING)
.collect(Collectors.toSet());

if (uncheckedWords.isEmpty()) {
Expand Down Expand Up @@ -201,10 +197,12 @@ protected void check() {
// check words and mark matched as checked
matches.stream()
.map(ruleMatch -> ruleMatch.getSentence().getTokens()[1].getToken())
.forEach(word -> checkedWordsForLang.put(word, true));
.forEach(word -> checkedWordsHolder.putWordStatus(lang, word, true));

// mark unmatched words without errors as checked
uncheckedWords.forEach(word -> checkedWordsForLang.putIfAbsent(word, false));
uncheckedWords.stream()
.filter(word -> checkedWordsHolder.getWordStatus(lang, word) == WordStatus.MISSING)
.forEach(word -> checkedWordsHolder.putWordStatus(lang, word, false));

fireDiagnosticOnCheckedWordsWithErrors(tokensMap);
}
Expand All @@ -213,10 +211,9 @@ private void fireDiagnosticOnCheckedWordsWithErrors(
Map<String, List<Token>> tokensMap
) {
String lang = info.getResourceString("diagnosticLanguage");
Map<String, Boolean> checkedWordsForLang = checkedWords.get(lang);

tokensMap.entrySet().stream()
.filter(entry -> checkedWordsForLang.getOrDefault(entry.getKey(), false))
.filter(entry -> checkedWordsHolder.getWordStatus(lang, entry.getKey()) == WordStatus.HAS_ERROR)
.forEach((Map.Entry<String, List<Token>> entry) -> {
String word = entry.getKey();
List<Token> tokens = entry.getValue();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* This file is a part of BSL Language Server.
*
* Copyright (c) 2018-2025
* Alexey Sosnoviy <[email protected]>, Nikita Fedkin <[email protected]> and contributors
*
* SPDX-License-Identifier: LGPL-3.0-or-later
*
* BSL Language Server is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3.0 of the License, or (at your option) any later version.
*
* BSL Language Server is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with BSL Language Server.
*/
package com.github._1c_syntax.bsl.languageserver.diagnostics.typo;

import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;

/**
* Component for managing persistent cache of checked words for typo diagnostic.
* Uses Spring Cache with EhCache for persistent disk storage.
*/
@Component
public class CheckedWordsHolder {

/**
* Get the status of a word from cache.
*
* @param lang language code ("en" or "ru")
* @param word the word to get status for
* @return WordStatus indicating if the word has an error, no error, or is missing from cache
*/
@Cacheable(value = "typoCache", key = "#lang + ':' + #word", cacheManager = "typoCacheManager")
public WordStatus getWordStatus(String lang, String word) {
return WordStatus.MISSING;
}

/**
* Store the status of a word in the cache.
*
* @param lang language code ("en" or "ru")
* @param word the word to store status for
* @param hasError true if the word has a typo, false otherwise
* @return the stored WordStatus
*/
@CachePut(value = "typoCache", key = "#lang + ':' + #word", cacheManager = "typoCacheManager")
public WordStatus putWordStatus(String lang, String word, boolean hasError) {
return hasError ? WordStatus.HAS_ERROR : WordStatus.NO_ERROR;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* This file is a part of BSL Language Server.
*
* Copyright (c) 2018-2025
* Alexey Sosnoviy <[email protected]>, Nikita Fedkin <[email protected]> and contributors
*
* SPDX-License-Identifier: LGPL-3.0-or-later
*
* BSL Language Server is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3.0 of the License, or (at your option) any later version.
*
* BSL Language Server is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with BSL Language Server.
*/
package com.github._1c_syntax.bsl.languageserver.diagnostics.typo;

import java.io.Serializable;

/**
* Status of a word's spell check result.
*/
public enum WordStatus implements Serializable {
/**
* Word has a spelling error.
*/
HAS_ERROR,

/**
* Word is spelled correctly.
*/
NO_ERROR,

/**
* Word has not been checked yet.
*/
MISSING
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,46 @@
package com.github._1c_syntax.bsl.languageserver.infrastructure;

import com.github.benmanes.caffeine.cache.Caffeine;
import com.github._1c_syntax.bsl.languageserver.diagnostics.typo.WordStatus;
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.CacheManagerBuilder;
import org.ehcache.config.builders.ResourcePoolsBuilder;
import org.ehcache.config.units.EntryUnit;
import org.ehcache.config.units.MemoryUnit;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.Cache;
import org.springframework.cache.Cache.ValueRetrievalException;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.cache.support.AbstractValueAdaptingCache;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.Callable;

/**
* Spring-конфигурация кэширования.
* <p>
* Для typoCache используется EhCache с персистентным хранилищем на диске.
* Для остальных кэшей (например, code lens) используется Caffeine с хранением в памяти.
*/
@Configuration
@EnableCaching
public class CacheConfiguration {

/**
* Основной менеджер кэша, использующий Caffeine для кэширования в памяти.
* <p>
* Помечен как {@code @Primary}, поэтому используется для всех кэшей по умолчанию,
* если не указан явно другой менеджер кэша (например, {@code typoCacheManager} для typoCache).
*/
@Bean
@Primary
public CacheManager cacheManager(Caffeine<Object, Object> caffeine) {
var caffeineCacheManager = new CaffeineCacheManager();
caffeineCacheManager.setCaffeine(caffeine);
Expand All @@ -45,4 +72,48 @@ public CacheManager cacheManager(Caffeine<Object, Object> caffeine) {
public Caffeine<Object, Object> caffeineConfig() {
return Caffeine.newBuilder();
}

/**
* Выделенный менеджер EhCache для typoCache с персистентным хранением на диске.
* <p>
* Настроен программно, без использования XML-конфигурации.
* При закрытии Spring-контекста вызывается метод {@code close()} для корректного завершения работы кэша.
*/
@Bean(destroyMethod = "close")
public org.ehcache.CacheManager ehcacheManager(
@Value("${app.cache.path}") String cacheDirPath
) {
var cacheDir = Path.of(cacheDirPath);

// Configure EhCache cache with disk persistence
var cacheConfig = CacheConfigurationBuilder
.newCacheConfigurationBuilder(
String.class,
WordStatus.class,
ResourcePoolsBuilder.newResourcePoolsBuilder()
.heap(100_000, EntryUnit.ENTRIES)
.disk(50, MemoryUnit.MB, true)
)
.build();

// Build native EhCache manager with persistence
return CacheManagerBuilder.newCacheManagerBuilder()
.with(CacheManagerBuilder.persistence(cacheDir.toFile()))
.withCache("typoCache", cacheConfig)
.build(true);
}

@Bean
public CacheManager typoCacheManager(org.ehcache.CacheManager ehcacheManager) {
var nativeCache = ehcacheManager.getCache("typoCache", String.class, WordStatus.class);

// Wrap the native cache with EhCacheAdapter
var simpleCacheManager = new SimpleCacheManager();
simpleCacheManager.setCaches(List.of(
new EhCacheAdapter<>(nativeCache, "typoCache")
));
simpleCacheManager.afterPropertiesSet();

return simpleCacheManager;
}
}
Loading
Loading