mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
avoid datarace in editor creation to fix IDEA-334190 Some .kt files are not highlighted from time to time in perf tests
AsyncEditorLoader used to store `null` to the editor user data, making `isEditorLoaded()` return `true`, then store not-null there, making `isEditorLoaded()` return `false`, then store `null` again, on loading finish. This created datarace when daemon tried to start highlighting (because it thought the editor was loaded) but then canceled itself. Now the transitions are: empty list(not loaded) -> non empty list of delayed tasks (still not loaded) -> LOADED GitOrigin-RevId: 171d2d48f30fd87dae3401f0493670717ba5d86f
This commit is contained in:
committed by
intellij-monorepo-bot
parent
c93a41c727
commit
3f6db3628a
+4
-2
@@ -18,6 +18,7 @@ import com.intellij.openapi.progress.ProcessCanceledException
|
||||
import com.intellij.openapi.progress.ProgressManager
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import kotlinx.coroutines.Deferred
|
||||
import java.awt.event.ComponentAdapter
|
||||
import java.awt.event.ComponentEvent
|
||||
import java.util.concurrent.CancellationException
|
||||
@@ -39,8 +40,9 @@ open class PsiAwareTextEditorImpl : TextEditorImpl {
|
||||
|
||||
internal constructor(project: Project,
|
||||
file: VirtualFile,
|
||||
asyncLoader: AsyncEditorLoader,
|
||||
editor: EditorImpl) : super(project = project, file = file, editor = editor, asyncLoader = asyncLoader)
|
||||
provider: TextEditorProvider,
|
||||
editor: EditorImpl,
|
||||
task: Deferred<Unit>) : super(project = project, file = file, editor = editor, provider = provider, asyncLoader = createAsyncEditorLoader(provider, project, editor, file, task))
|
||||
|
||||
override fun createEditorComponent(project: Project, file: VirtualFile, editor: EditorImpl): TextEditorComponent {
|
||||
val component = PsiAwareTextEditorComponent(project = project, file = file, textEditor = this, editor = editor)
|
||||
|
||||
+3
-7
@@ -23,7 +23,6 @@ import com.intellij.openapi.editor.impl.EditorGutterLayout
|
||||
import com.intellij.openapi.extensions.ExtensionPointName
|
||||
import com.intellij.openapi.fileEditor.*
|
||||
import com.intellij.openapi.fileEditor.impl.text.AsyncEditorLoader.Companion.isEditorLoaded
|
||||
import com.intellij.openapi.fileEditor.impl.text.TextEditorImpl.Companion.createAsyncEditorLoader
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.WriteExternalException
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
@@ -45,8 +44,6 @@ open class PsiAwareTextEditorProvider : TextEditorProvider(), AsyncFileEditorPro
|
||||
}
|
||||
|
||||
override suspend fun createEditorBuilder(project: Project, file: VirtualFile, document: Document?): AsyncFileEditorProvider.Builder {
|
||||
val asyncLoader = createAsyncEditorLoader(provider = this, project = project)
|
||||
|
||||
val effectiveDocument = if (document == null) {
|
||||
val fileDocumentManager = serviceAsync<FileDocumentManager>()
|
||||
fileDocumentManager.getCachedDocument(file) ?: readActionBlocking {
|
||||
@@ -76,7 +73,7 @@ open class PsiAwareTextEditorProvider : TextEditorProvider(), AsyncFileEditorPro
|
||||
|
||||
val editorDeferred = CompletableDeferred<EditorEx>()
|
||||
|
||||
val task = asyncLoader.coroutineScope.async(CoroutineName("TextEditorInitializer")) {
|
||||
val task = TextEditorImpl.editorLoaderScope(project).async(CoroutineName("call TextEditorInitializers")) {
|
||||
val editorSupplier = suspend { editorDeferred.await() }
|
||||
val highlighterReady = suspend { highlighterDeferred.join() }
|
||||
|
||||
@@ -122,9 +119,8 @@ open class PsiAwareTextEditorProvider : TextEditorProvider(), AsyncFileEditorPro
|
||||
override fun build(): FileEditor {
|
||||
val editor = factory.createMainEditor(effectiveDocument, project, file, highlighter)
|
||||
editor.gutterComponentEx.setInitialIconAreaWidth(EditorGutterLayout.getInitialGutterWidth())
|
||||
val textEditor = PsiAwareTextEditorImpl(project = project, file = file, editor = editor, asyncLoader = asyncLoader)
|
||||
editorDeferred.complete(textEditor.editor)
|
||||
asyncLoader.start(textEditor = textEditor, tasks = listOf(task))
|
||||
editorDeferred.complete(editor)
|
||||
val textEditor = PsiAwareTextEditorImpl(project = project, file = file, provider = this@PsiAwareTextEditorProvider, editor = editor, task = task)
|
||||
return textEditor
|
||||
}
|
||||
}
|
||||
|
||||
+69
-23
@@ -8,7 +8,10 @@ import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.application.asContextElement
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.editor.ex.EditorEx
|
||||
import com.intellij.openapi.editor.impl.EditorImpl
|
||||
import com.intellij.openapi.fileEditor.FileEditorManager
|
||||
import com.intellij.openapi.fileEditor.FileEditorStateLevel
|
||||
import com.intellij.openapi.fileEditor.TextEditor
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.Key
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
@@ -20,7 +23,6 @@ import com.intellij.util.concurrency.annotations.RequiresReadLock
|
||||
import com.intellij.util.ui.AsyncProcessIcon
|
||||
import kotlinx.coroutines.*
|
||||
import org.jetbrains.annotations.ApiStatus.Internal
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.event.ChangeEvent
|
||||
@@ -28,29 +30,56 @@ import kotlin.coroutines.resume
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
private val ASYNC_LOADER = Key.create<AsyncEditorLoader>("ASYNC_LOADER")
|
||||
|
||||
class AsyncEditorLoader internal constructor(private val project: Project,
|
||||
private val provider: TextEditorProvider,
|
||||
@JvmField val coroutineScope: CoroutineScope) {
|
||||
private val delayedActions = ArrayDeque<Runnable>()
|
||||
@JvmField val coroutineScope: CoroutineScope,
|
||||
editor: EditorImpl,
|
||||
virtualFile: VirtualFile, task: Deferred<Unit>?) {
|
||||
private val tasks: List<Deferred<Unit>>
|
||||
init {
|
||||
val textEditorInit = coroutineScope.async(CoroutineName("HighlighterTextEditorInitializer")) {
|
||||
TextEditorImpl.setHighlighterToEditor(project, virtualFile, editor.document, editor)
|
||||
}
|
||||
tasks = if (task == null) listOf(textEditorInit) else listOf(textEditorInit, task)
|
||||
}
|
||||
/**
|
||||
* [delayedActions] contains either:
|
||||
* - empty list: the editor was not loaded
|
||||
* - list of runnables: the editor was not loaded and these runnables need to be run on load
|
||||
* - [LOADED]: the editor is loaded
|
||||
* empty list was chosen to mark editor "not loaded" as early as possible, to avoid a narrow data race between TextEditorImpl instantiation and [AsyncEditorLoader.start] call
|
||||
*/
|
||||
private val LOADED: List<Runnable> = listOf(Runnable {})
|
||||
private val delayedActions: AtomicReference<List<Runnable>> = AtomicReference(listOf())
|
||||
private val delayedScrollState = AtomicReference<DelayedScrollState?>()
|
||||
|
||||
companion object {
|
||||
internal val OPENED_IN_BULK = Key.create<Boolean>("EditorSplitters.opened.in.bulk")
|
||||
internal val OPENED_IN_BULK: Key<Boolean> = Key.create("EditorSplitters.opened.in.bulk")
|
||||
|
||||
@Internal
|
||||
fun isOpenedInBulk(file: VirtualFile): Boolean = file.getUserData(OPENED_IN_BULK) != null
|
||||
|
||||
private fun findTextEditor(editor: Editor): TextEditor? {
|
||||
val project = editor.project
|
||||
val virtualFile = editor.virtualFile
|
||||
if (project == null || virtualFile == null) return null
|
||||
return FileEditorManager.getInstance(project).getAllEditors(virtualFile).find { f -> f is TextEditor } as TextEditor?
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@RequiresEdt
|
||||
fun performWhenLoaded(editor: Editor, runnable: Runnable) {
|
||||
val loader = editor.getUserData(ASYNC_LOADER)
|
||||
loader?.delayedActions?.add(captureThreadContext(runnable)) ?: runnable.run()
|
||||
val asyncLoader = (findTextEditor(editor) as? TextEditorImpl)?.asyncLoader
|
||||
if (asyncLoader == null) {
|
||||
runnable.run()
|
||||
}
|
||||
else {
|
||||
asyncLoader.performWhenLoaded(runnable)
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun waitForLoaded(editor: Editor) {
|
||||
if (editor.getUserData(ASYNC_LOADER) != null) {
|
||||
if (!isEditorLoaded(editor)) {
|
||||
withContext(Dispatchers.EDT + ModalityState.any().asContextElement()) {
|
||||
suspendCancellableCoroutine {
|
||||
performWhenLoaded(editor) { it.resume(Unit) }
|
||||
@@ -61,19 +90,32 @@ class AsyncEditorLoader internal constructor(private val project: Project,
|
||||
|
||||
@JvmStatic
|
||||
fun isEditorLoaded(editor: Editor): Boolean {
|
||||
return editor.getUserData(ASYNC_LOADER) == null
|
||||
val textEditor = findTextEditor(editor)
|
||||
if (textEditor !is TextEditorImpl) return true
|
||||
return textEditor.isLoaded()
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
private fun performWhenLoaded(runnable: Runnable) {
|
||||
val toRunLater = captureThreadContext(runnable)
|
||||
val newActions = delayedActions.updateAndGet { oldActions: List<Runnable> ->
|
||||
if (oldActions == LOADED || oldActions.contains(toRunLater)) oldActions
|
||||
else oldActions + toRunLater
|
||||
}
|
||||
if (!newActions.contains(toRunLater)) {
|
||||
runnable.run()
|
||||
}
|
||||
}
|
||||
|
||||
// executed in the same EDT task where TextEditorImpl is created
|
||||
@Internal
|
||||
@RequiresEdt
|
||||
fun start(textEditor: TextEditorImpl, tasks: List<Deferred<*>>) {
|
||||
fun start(textEditor: TextEditorImpl) {
|
||||
val editor = textEditor.editor
|
||||
editor.putUserData(ASYNC_LOADER, this)
|
||||
|
||||
if (ApplicationManager.getApplication().isUnitTestMode) {
|
||||
startInTests(tasks = tasks, editor = editor)
|
||||
startInTests(tasks = tasks)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -89,12 +131,9 @@ class AsyncEditorLoader internal constructor(private val project: Project,
|
||||
indicatorJob.cancel()
|
||||
|
||||
withContext(Dispatchers.EDT + CoroutineName("execute delayed actions")) {
|
||||
editor.putUserData(ASYNC_LOADER, null)
|
||||
editor.scrollingModel.disableAnimation()
|
||||
try {
|
||||
while (true) {
|
||||
(delayedActions.pollFirst() ?: break).run()
|
||||
}
|
||||
markLoadedAndExecuteDelayedActions()
|
||||
}
|
||||
finally {
|
||||
editor.scrollingModel.enableAnimation()
|
||||
@@ -104,15 +143,18 @@ class AsyncEditorLoader internal constructor(private val project: Project,
|
||||
}
|
||||
}
|
||||
|
||||
private fun startInTests(tasks: List<Deferred<*>>, editor: EditorEx) {
|
||||
private fun markLoadedAndExecuteDelayedActions() {
|
||||
val delayedActions = delayedActions.getAndSet(LOADED)
|
||||
for (action in delayedActions) {
|
||||
action.run()
|
||||
}
|
||||
}
|
||||
|
||||
private fun startInTests(tasks: List<Deferred<*>>) {
|
||||
runWithModalProgressBlocking(project, "") {
|
||||
tasks.awaitAll()
|
||||
}
|
||||
editor.putUserData(ASYNC_LOADER, null)
|
||||
|
||||
while (true) {
|
||||
(delayedActions.pollFirst() ?: break).run()
|
||||
}
|
||||
markLoadedAndExecuteDelayedActions()
|
||||
}
|
||||
|
||||
@RequiresReadLock
|
||||
@@ -136,6 +178,10 @@ class AsyncEditorLoader internal constructor(private val project: Project,
|
||||
internal fun dispose() {
|
||||
coroutineScope.cancel()
|
||||
}
|
||||
|
||||
internal fun isLoaded(): Boolean {
|
||||
return delayedActions.get() == LOADED
|
||||
}
|
||||
}
|
||||
|
||||
private class DelayedScrollState(@JvmField val relativeCaretPosition: Int, @JvmField val exactState: Boolean)
|
||||
|
||||
+30
-34
@@ -33,10 +33,9 @@ import com.intellij.platform.diagnostic.telemetry.impl.span
|
||||
import com.intellij.platform.util.coroutines.childScope
|
||||
import com.intellij.pom.Navigatable
|
||||
import com.intellij.psi.PsiDocumentManager
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import org.jetbrains.annotations.ApiStatus.Internal
|
||||
import org.jetbrains.annotations.NonNls
|
||||
import java.beans.PropertyChangeListener
|
||||
@@ -46,33 +45,20 @@ import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
private val TRANSIENT_EDITOR_STATE_KEY = Key.create<TransientEditorState>("transientState")
|
||||
|
||||
open class TextEditorImpl @Internal constructor(@JvmField protected val project: Project,
|
||||
@JvmField protected val file: VirtualFile,
|
||||
editor: EditorImpl,
|
||||
private val asyncLoader: AsyncEditorLoader) : UserDataHolderBase(), TextEditor {
|
||||
open class TextEditorImpl
|
||||
@Internal @JvmOverloads constructor(@JvmField protected val project: Project,
|
||||
@JvmField protected val file: VirtualFile,
|
||||
provider: TextEditorProvider,
|
||||
editor: EditorImpl,
|
||||
internal val asyncLoader: AsyncEditorLoader = createAsyncEditorLoader(provider, project, editor, file)) : UserDataHolderBase(), TextEditor {
|
||||
@Suppress("LeakingThis")
|
||||
private val changeSupport = PropertyChangeSupport(this)
|
||||
private val component: TextEditorComponent
|
||||
|
||||
constructor(project: Project,
|
||||
file: VirtualFile,
|
||||
provider: TextEditorProvider,
|
||||
editor: EditorImpl) : this(project = project,
|
||||
file = file,
|
||||
editor = editor,
|
||||
asyncLoader = createAsyncEditorLoader(provider, project)) {
|
||||
val editorSupplier = suspend { editor }
|
||||
@Suppress("LeakingThis")
|
||||
asyncLoader.start(textEditor = this, tasks = listOf(
|
||||
asyncLoader.coroutineScope.async(CoroutineName("HighlighterTextEditorInitializer")) {
|
||||
setHighlighterToEditor(project, file, editor.document, editorSupplier)
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
init {
|
||||
@Suppress("LeakingThis")
|
||||
component = createEditorComponent(project = project, file = file, editor = editor)
|
||||
asyncLoader.start(this)
|
||||
for (customizer in TextEditorCustomizer.EP.extensionList) {
|
||||
@Suppress("LeakingThis")
|
||||
customizer.customize(this)
|
||||
@@ -89,18 +75,25 @@ open class TextEditorImpl @Internal constructor(@JvmField protected val project:
|
||||
|
||||
// don't pollute global scope
|
||||
companion object {
|
||||
fun createAsyncEditorLoader(provider: TextEditorProvider, project: Project): AsyncEditorLoader {
|
||||
fun createAsyncEditorLoader(provider: TextEditorProvider, project: Project, editor: EditorImpl, virtualFile: VirtualFile, task: Deferred<Unit>? = null): AsyncEditorLoader {
|
||||
return AsyncEditorLoader(
|
||||
project = project,
|
||||
provider = provider,
|
||||
coroutineScope = editorLoaderScope(project),
|
||||
editor, virtualFile, task
|
||||
)
|
||||
}
|
||||
|
||||
@Internal
|
||||
fun editorLoaderScope(project: Project): CoroutineScope {
|
||||
// `openEditorImpl` uses runWithModalProgressBlocking,
|
||||
// but an async editor load is performed in the background, out of the `openEditorImpl` call
|
||||
val modality = ModalityState.any().asContextElement()
|
||||
|
||||
val context = if (StartUpMeasurer.isEnabled()) rootTask() else EmptyCoroutineContext
|
||||
return AsyncEditorLoader(
|
||||
project = project,
|
||||
provider = provider,
|
||||
coroutineScope = project.service<AsyncEditorLoaderService>().coroutineScope.childScope(supervisor = false,
|
||||
context = context + modality),
|
||||
)
|
||||
val coroutineScope = project.service<AsyncEditorLoaderService>().coroutineScope.childScope(supervisor = false,
|
||||
context = context + modality)
|
||||
return coroutineScope
|
||||
}
|
||||
|
||||
@Internal
|
||||
@@ -122,10 +115,10 @@ open class TextEditorImpl @Internal constructor(@JvmField protected val project:
|
||||
return factory.createMainEditor(document!!, project, file, null)
|
||||
}
|
||||
|
||||
private suspend fun setHighlighterToEditor(project: Project,
|
||||
file: VirtualFile,
|
||||
document: Document,
|
||||
editorSupplier: suspend () -> EditorEx) {
|
||||
internal suspend fun setHighlighterToEditor(project: Project,
|
||||
file: VirtualFile,
|
||||
document: Document,
|
||||
editor: EditorImpl) {
|
||||
val scheme = serviceAsync<EditorColorsManager>().globalScheme
|
||||
val editorHighlighterFactory = serviceAsync<EditorHighlighterFactory>()
|
||||
val highlighter = readAction {
|
||||
@@ -136,7 +129,6 @@ open class TextEditorImpl @Internal constructor(@JvmField protected val project:
|
||||
highlighter
|
||||
}
|
||||
|
||||
val editor = editorSupplier()
|
||||
span("editor highlighter set", Dispatchers.EDT) {
|
||||
editor.settings.setLanguageSupplier { getDocumentLanguage(editor) }
|
||||
editor.highlighter = highlighter
|
||||
@@ -215,6 +207,10 @@ open class TextEditorImpl @Internal constructor(@JvmField protected val project:
|
||||
}
|
||||
|
||||
override fun toString(): @NonNls String = "Editor: ${component.file}"
|
||||
|
||||
internal fun isLoaded(): Boolean {
|
||||
return asyncLoader.isLoaded()
|
||||
}
|
||||
}
|
||||
|
||||
private class TransientEditorState {
|
||||
|
||||
@@ -17,6 +17,7 @@ import com.intellij.openapi.editor.impl.view.FontLayoutService;
|
||||
import com.intellij.openapi.editor.markup.HighlighterTargetArea;
|
||||
import com.intellij.openapi.editor.markup.RangeHighlighter;
|
||||
import com.intellij.openapi.editor.markup.TextAttributes;
|
||||
import com.intellij.openapi.fileEditor.impl.text.AsyncEditorLoader;
|
||||
import com.intellij.openapi.ide.CopyPasteManager;
|
||||
import com.intellij.openapi.util.Ref;
|
||||
import com.intellij.openapi.util.SystemInfo;
|
||||
@@ -431,6 +432,7 @@ public class EditorImplTest extends AbstractEditorTest {
|
||||
|
||||
public void testDefaultHorizontalScrolling() {
|
||||
initText("<caret>" + StringUtil.repeat("abc", 100));
|
||||
assertTrue(AsyncEditorLoader.Companion.isEditorLoaded(getEditor()));
|
||||
int spaceWidth = EditorUtil.getSpaceWidth(Font.PLAIN, getEditor());
|
||||
EditorTestUtil.setEditorVisibleSize(getEditor(), 15, 2);
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.openapi.editor.impl;
|
||||
|
||||
import com.intellij.diagnostic.ThreadDumper;
|
||||
import com.intellij.openapi.application.ApplicationManager;
|
||||
import com.intellij.openapi.editor.Editor;
|
||||
import com.intellij.openapi.editor.EditorFactory;
|
||||
import com.intellij.openapi.editor.event.EditorFactoryEvent;
|
||||
import com.intellij.openapi.editor.event.EditorFactoryListener;
|
||||
import com.intellij.openapi.fileEditor.FileEditor;
|
||||
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
|
||||
import com.intellij.openapi.fileEditor.impl.text.AsyncEditorLoader;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.intellij.testFramework.FileEditorManagerTestCase;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public class EditorLoadTest extends FileEditorManagerTestCase {
|
||||
public void testEditorLoadedStateMustTransitionInOneDirection_Stress() throws Exception {
|
||||
VirtualFile virtualFile = createTempFile(getTestName(false) + ".txt", "text".getBytes(StandardCharsets.UTF_8));
|
||||
AtomicReference<Future<?>> testStateInvariant = new AtomicReference<>();
|
||||
AtomicBoolean run = new AtomicBoolean();
|
||||
AtomicBoolean loaded = new AtomicBoolean();
|
||||
EditorFactory.getInstance().addEditorFactoryListener(new EditorFactoryListener() {
|
||||
@Override
|
||||
public void editorCreated(@NotNull EditorFactoryEvent event) {
|
||||
Editor editor = event.getEditor();
|
||||
testStateInvariant.set(ApplicationManager.getApplication().executeOnPooledThread(() -> {
|
||||
while (run.get()) {
|
||||
boolean markedLoaded = AsyncEditorLoader.Companion.isEditorLoaded(editor);
|
||||
boolean oldLoaded = loaded.getAndSet(markedLoaded);
|
||||
if (oldLoaded && !markedLoaded) {
|
||||
throw new AssertionError("incorrect state transition: loaded->!loaded\nthreaddump:\n" + ThreadDumper.dumpThreadsToString());
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}, getTestRootDisposable());
|
||||
for (int i=0; i<100; i++) {
|
||||
testStateInvariant.set(null);
|
||||
run.set(true);
|
||||
loaded.set(false);
|
||||
List<FileEditor> editors = manager.openEditor(new OpenFileDescriptor(getProject(), virtualFile, 0), false);
|
||||
LOG.debug(i+": "+editors);
|
||||
System.out.println(i+": "+editors);
|
||||
try {
|
||||
assertTrue(loaded.get());
|
||||
run.set(false);
|
||||
testStateInvariant.get().get();
|
||||
}
|
||||
finally {
|
||||
manager.closeFile(virtualFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user