diff --git a/README.md b/README.md index 6b747d0..07935c9 100644 --- a/README.md +++ b/README.md @@ -3,53 +3,70 @@ ## 핵심 기능 정의 --- -### ☑️ 메모 저장 기능 + +### ✅메모 저장 기능 + - 메모를 저장할 수 있어야 한다. - 메모의 저장 데이터는 다음과 같다. - - `int id (not null)`: 메모의 고유한 아이디(PK) - - `String content (nullable)`: 사용자가 입력하는 메모의 내용 - - `LocalDateTime createdAt (not null)`: 메모가 생성된 시간 - - `LocalDateTime updatedAt (not null)`: 메모가 수정된 시간 - - `String commitHash (nullable)`: 현재 커밋의 해시값 - - `String filePath (nullable)`: 파일 경로 - - `String selectedCodeSnippet (nullable)`: 선택된 코드 - - `long selectionEnd (nullable)`: 문서에서 선택한 정확한 종료 위치 - - `long selectionStart (nullable)`: 문서에서 선택한 정확한 시작 위치 - - `int visibleEnd (nullable)`: 선택한 종료 줄 - - `int visibleStart (nullable)`: 선택한 시작 줄 + - `int id (not null)`: 메모의 고유한 아이디(PK) + - `String content (not null)`: 사용자가 입력하는 메모의 내용 + - `LocalDateTime createdAt (not null)`: 메모가 생성된 시간 + - `LocalDateTime updatedAt (not null)`: 메모가 수정된 시간 + - `String commitHash (nullable)`: 현재 커밋의 해시값 + - `String filePath (nullable)`: 파일 경로 + - `String selectedCodeSnippet (nullable)`: 선택된 코드 + - `long selectionEnd (nullable)`: 문서에서 선택한 정확한 종료 위치 + - `long selectionStart (nullable)`: 문서에서 선택한 정확한 시작 위치 + - `int visibleEnd (nullable)`: 선택한 종료 줄 + - `int visibleStart (nullable)`: 선택한 시작 줄 - 저장은 시간 순서대로 저장이 되어야 한다. -### ⚠️ 메모 저장 기능 예외 상황 +### ⚠️ 메모 저장 기능 제약 상황 + +- 메모의 `content`가 blank라면 저장이 되어선 안된다. +- `selectionStart`가 `selectionEnd`보다 앞에 있어야 한다. +- `visibleStart`가 `visibleEnd`보다 앞에 있어야 한다. +- `createdAt`은 자동으로 생성 되어야 한다. + +--- ### ☑️ 메모 조회 기능 + - 메모의 id를 이용해서 데이터를 조회할 수 있어야 한다. - 전체 메모를 날짜순으로 조회할 수 있어야 한다. -- + +### ⚠️ 메모 조회 기능 예외 상황 --- + ### ☑️ 메모 삭제 기능 + - 저장된 메모를 삭제할 수 있어야 한다. - 메모를 한 번에 여러개 삭제할 수 있어야 한다. ### ⚠️ 메모 삭제 기능 예외 상황 --- + ### ☑️ 메모 수정 기능 + - 저장된 메모의 content를 수정할 수 있어야 한다. ### ⚠️ 메모 수정 기능 예외 상황 --- + ### ☑️ 메모 추출 기능 + - 저장된 메모를 한 개 이상 선택하여 txt 파일로 추출할 수 있어야 한다. - 추출한 단위 메모의 구성 내용은 다음과 같다. - - 메모 순서(시간순) - - `LocalDateTime timestamp`: 메모가 생성된 시간 - - `String content`: 사용자가 입력하는 메모의 내용 - - `String filePath`: 파일 경로 - - `String commitHash`: 현재 커밋의 해시값 - - `int visibleStart`: 선택한 시작 줄 - - `int visibleEnd`: 선택한 종료 줄 + - 메모 순서(시간순) + - `LocalDateTime timestamp`: 메모가 생성된 시간 + - `String content`: 사용자가 입력하는 메모의 내용 + - `String filePath`: 파일 경로 + - `String commitHash`: 현재 커밋의 해시값 + - `int visibleStart`: 선택한 시작 줄 + - `int visibleEnd`: 선택한 종료 줄 - txt 파일 상단에 프로젝트명, 내보낸 시각, 메모의 개수가 있어야 한다. - txt 파일의 이름은 `devlog-{프로젝트명}-{내보낸날짜}-{내보낸시각}.txt` 이다. - 추출할 메모가 없으면 빈 txt 파일을 반환한다. @@ -57,28 +74,34 @@ ### ⚠️ 메모 추출 기능 예외 상황 --- + ### ☑️ 노트 수정/저장 기능 + - 노트를 저장할 수 있어야 한다. - 노트의 수정/저장 데이터는 다음과 같다. - - `String content`: 노트의 내용 - - `LocalDateTime savedAt`: 저장된 시각 + - `String content`: 노트의 내용 + - `LocalDateTime savedAt`: 저장된 시각 ### ⚠️ 노트 수정/저장 기능 예외 상황 --- + ## 화면 요구 사항 --- + ### ☑️ 플러그인 기본 화면 + - 화면의 최상단엔 기본 조작용 버튼이 있어야 한다. - 기본 조작용 버튼은 아래와 같다. - - 메모목록/노트 화면 전환 버튼 - - 메모 전체 선택/선택해제 버튼 - - 선택된 메모 추출 버튼 - - 선택된 메모 삭제 버튼 + - 메모목록/노트 화면 전환 버튼 + - 메모 전체 선택/선택해제 버튼 + - 선택된 메모 추출 버튼 + - 선택된 메모 삭제 버튼 - 메인 컨텐츠를 표시하는 화면이 있어야 한다. ### ☑️ 메모 목록 출력 화면 + - 저장된 메모 전체가 최근순 정렬되어 화면에 보여야 한다. - 각 메모 좌측에는 메모를 선택할 수 있는 체크박스가 있어야 한다. - 화면 하단에는 새 메모를 적을 수 있는 텍스트 입력창이 있어야 한다. @@ -89,7 +112,16 @@ ### ⚠️ 메모 목록 출력 화면 예외 상황 --- + ### ☑️ 노트 출력 화면 + - 저장된 노트의 모든 내용이 출력 되어야 한다. - 노트의 변경 내용은 자동 저장 되어야 한다. - 화면의 크기를 벗어나지 않게 내용을 출력해야 한다. + +### ⚠️ 노트 출력 화면 예외 상황 + + +DevLog Plugin is a memo/note tracking plugin that automatically captures your code selection, +file path, commit hash, and editor state, storing it for later use. + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 6cbe4b2..df4a29b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,9 +34,18 @@ dependencies { testImplementation(libs.junit) testImplementation(libs.opentest4j) + testImplementation(kotlin("test")) + testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0") + // IntelliJ Platform Gradle Plugin Dependencies Extension - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-dependencies-extension.html intellijPlatform { - create(providers.gradleProperty("platformType"), providers.gradleProperty("platformVersion")) + create( + providers.gradleProperty("platformType"), + providers.gradleProperty("platformVersion") + ) + + bundledPlugins("Git4Idea") // Plugin Dependencies. Uses `platformBundledPlugins` property from the gradle.properties file for bundled IntelliJ Platform plugins. bundledPlugins(providers.gradleProperty("platformBundledPlugins").map { it.split(',') }) @@ -99,7 +108,8 @@ intellijPlatform { // The pluginVersion is based on the SemVer (https://semver.org) and supports pre-release labels, like 2.1.7-alpha.3 // Specify pre-release label to publish the plugin in a custom Release Channel automatically. Read more: // https://plugins.jetbrains.com/docs/intellij/deployment.html#specifying-a-release-channel - channels = providers.gradleProperty("pluginVersion").map { listOf(it.substringAfter('-', "").substringBefore('.').ifEmpty { "default" }) } + channels = providers.gradleProperty("pluginVersion") + .map { listOf(it.substringAfter('-', "").substringBefore('.').ifEmpty { "default" }) } } pluginVerification { diff --git a/com/intellij/testFramework/ServiceContainerUtil.kt b/com/intellij/testFramework/ServiceContainerUtil.kt new file mode 100644 index 0000000..22d3261 --- /dev/null +++ b/com/intellij/testFramework/ServiceContainerUtil.kt @@ -0,0 +1,118 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +@file:JvmName("ServiceContainerUtil") +package com.intellij.testFramework + +import com.intellij.ide.plugins.IdeaPluginDescriptorImpl +import com.intellij.ide.plugins.PluginManagerCore +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.Application +import com.intellij.openapi.components.ComponentManager +import com.intellij.openapi.components.ServiceDescriptor +import com.intellij.openapi.extensions.BaseExtensionPointName +import com.intellij.openapi.extensions.DefaultPluginDescriptor +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer +import com.intellij.serviceContainer.ComponentManagerImpl +import com.intellij.util.messages.ListenerDescriptor +import com.intellij.util.messages.MessageBusOwner +import org.jetbrains.annotations.TestOnly + +private val testDescriptor by lazy { DefaultPluginDescriptor("test") } + +@TestOnly +fun ComponentManager.registerServiceInstance(serviceInterface: Class, instance: T) { + (this as ComponentManagerImpl).registerServiceInstance(serviceInterface, instance, testDescriptor) +} + +/** + * Unregister service specified by [serviceInterface] if it was registered; + * throws [IllegalStateException] if the service was not registered. + */ +@TestOnly +fun ComponentManager.unregisterService(serviceInterface: Class<*>) { + (this as ComponentManagerImpl).unregisterService(serviceInterface) +} + +/** + * Register a new service or replace an existing service with a specified instance for testing purposes. + * Registration will be rolled back when parentDisposable is disposed. In most of the cases, + * [com.intellij.testFramework.UsefulTestCase.getTestRootDisposable] should be specified. + */ +@TestOnly +fun ComponentManager.registerOrReplaceServiceInstance(serviceInterface: Class, instance: T, parentDisposable: Disposable) { + val previous = this.getService(serviceInterface) + if (previous != null) { + replaceService(serviceInterface, instance, parentDisposable) + } + else { + (this as ComponentManagerImpl).registerServiceInstance(serviceInterface, instance, testDescriptor) + if (instance is Disposable) { + Disposer.register(parentDisposable, instance) + } + else { + Disposer.register(parentDisposable) { + this.unregisterComponent(serviceInterface) + } + } + } +} + +@TestOnly +fun ComponentManager.replaceService(serviceInterface: Class, instance: T, parentDisposable: Disposable) { + (this as ComponentManagerImpl).replaceServiceInstance(serviceInterface, instance, parentDisposable) +} + +@TestOnly +fun ComponentManager.registerComponentInstance(componentInterface: Class, instance: T, parentDisposable: Disposable?) { + (this as ComponentManagerImpl).replaceComponentInstance(componentInterface, instance, parentDisposable) +} + +@TestOnly +@JvmOverloads +fun ComponentManager.registerComponentImplementation(key: Class<*>, implementation: Class<*>, shouldBeRegistered: Boolean = false) { + (this as ComponentManagerImpl).registerComponentImplementation(key, implementation, shouldBeRegistered) +} + +@TestOnly +fun ComponentManager.registerExtension(name: BaseExtensionPointName<*>, instance: T, parentDisposable: Disposable) { + extensionArea.getExtensionPoint(name.name).registerExtension(instance, parentDisposable) +} + +@TestOnly +fun ComponentManager.getServiceImplementationClassNames(prefix: String): List { + val result = ArrayList() + processAllServiceDescriptors(this) { serviceDescriptor -> + val implementation = serviceDescriptor.implementation ?: return@processAllServiceDescriptors + if (implementation.startsWith(prefix)) { + result.add(implementation) + } + } + return result +} + +fun processAllServiceDescriptors(componentManager: ComponentManager, consumer: (ServiceDescriptor) -> Unit) { + for (plugin in PluginManagerCore.loadedPlugins) { + val pluginDescriptor = plugin as IdeaPluginDescriptorImpl + val containerDescriptor = when (componentManager) { + is Application -> pluginDescriptor.appContainerDescriptor + is Project -> pluginDescriptor.projectContainerDescriptor + else -> pluginDescriptor.moduleContainerDescriptor + } + containerDescriptor.services.forEach { + if ((componentManager as? ComponentManagerImpl)?.isServiceSuitable(it) != false && + (it.os == null || componentManager.isSuitableForOs(it.os))) { + consumer(it) + } + } + } +} + +fun createSimpleMessageBusOwner(owner: String): MessageBusOwner { + return object : MessageBusOwner { + override fun createListener(descriptor: ListenerDescriptor) = throw UnsupportedOperationException() + + override fun isDisposed() = false + + override fun toString() = owner + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/yeoli/devlog/domain/memo/domain/Memo.kt b/src/main/kotlin/com/github/yeoli/devlog/domain/memo/domain/Memo.kt new file mode 100644 index 0000000..626b566 --- /dev/null +++ b/src/main/kotlin/com/github/yeoli/devlog/domain/memo/domain/Memo.kt @@ -0,0 +1,43 @@ +package com.github.yeoli.devlog.domain.memo.domain + +import java.time.LocalDateTime + +class Memo( + val content: String, + + val commitHash: String? = null, + val filePath: String? = null, + val selectedCodeSnippet: String? = null, + + val selectionStart: Int? = null, + val selectionEnd: Int? = null, + + val visibleStart: Int? = null, + val visibleEnd: Int? = null +) { + val id: Long = generateId() + val createdAt: LocalDateTime = LocalDateTime.now() + val updatedAt: LocalDateTime = LocalDateTime.now() + + init { + validate() + } + + private fun validate() { + if (selectionStart != null && selectionEnd != null) { + require(selectionStart <= selectionEnd) { + "selectionStart는 selectionEnd 보다 작아야합니다." + } + } + + if (visibleStart != null && visibleEnd != null) { + require(visibleStart <= visibleEnd) { + "visibleStart는 visibleEnd 보다 작아야합니다." + } + } + } + + private fun generateId(): Long { + return System.currentTimeMillis() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/yeoli/devlog/domain/memo/service/MemoService.kt b/src/main/kotlin/com/github/yeoli/devlog/domain/memo/service/MemoService.kt new file mode 100644 index 0000000..438465b --- /dev/null +++ b/src/main/kotlin/com/github/yeoli/devlog/domain/memo/service/MemoService.kt @@ -0,0 +1,77 @@ +package com.github.yeoli.devlog.domain.memo.service + +import com.github.yeoli.devlog.domain.memo.domain.Memo +import com.ibm.icu.impl.IllegalIcuArgumentException +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.project.Project +import git4idea.repo.GitRepositoryManager +import java.awt.Point + +class MemoService { + + private val logger = Logger.getInstance(MemoService::class.java) + + fun createMemo(content: String, project: Project): Memo? { + val editor = getActiveEditor(project) + if (editor == null) { + logger.warn("[createMemo] editor가 null이므로 null을 반환합니다.") + return null + } + + val selectionModel = editor.selectionModel + val document = editor.document + + val selectedCodeSnippet = selectionModel.selectedText + + val selectionStart = selectionModel.selectionStart + val selectionEnd = selectionModel.selectionEnd + + val visibleArea = editor.scrollingModel.visibleAreaOnScrollingFinished + val visibleStartLine = editor.xyToLogicalPosition(visibleArea.location).line + val visibleEndLine = editor.xyToLogicalPosition( + Point(visibleArea.x, visibleArea.y + visibleArea.height) + ).line + + val virtualFile = FileDocumentManager.getInstance().getFile(document) + val filePath = virtualFile?.path + + val commitHash = getCurrentCommitHash(project) + + if (content.isBlank()) { + logger.warn("[createMemo] content가 blanck 이므로 null을 반환합니다.") + return null + } + + val memo: Memo + try { + memo = Memo( + content = content, + commitHash = commitHash, + filePath = filePath, + selectedCodeSnippet = selectedCodeSnippet, + selectionStart = selectionStart, + selectionEnd = selectionEnd, + visibleStart = visibleStartLine, + visibleEnd = visibleEndLine + ) + } catch (e: IllegalIcuArgumentException) { + logger.warn("[createMemo] Memo 생성에 실패하여 null을 반환합니다.(사유: " + e.message + ")") + return null; + } + + return memo + } + + private fun getActiveEditor(project: Project): Editor? { + return FileEditorManager.getInstance(project).selectedTextEditor + } + + private fun getCurrentCommitHash(project: Project): String? { + val repoManager = GitRepositoryManager.getInstance(project) + val repo = repoManager.repositories.firstOrNull() ?: return null + return repo.currentRevision + } +} \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 533e524..a8decb7 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -1,15 +1,17 @@ - com.github.yeoli.devlog - dev-log - yeo-li + com.intellij.modules.platform + Git4Idea + + + + - com.intellij.modules.platform + com.github.yeoli.devlog + dev-log - messages.MyBundle + messages.MyBundle - - - - + yeo-li diff --git a/src/test/kotlin/com/github/yeoli/devlog/domain/memo/domain/MemoTest.kt b/src/test/kotlin/com/github/yeoli/devlog/domain/memo/domain/MemoTest.kt new file mode 100644 index 0000000..01ef4c5 --- /dev/null +++ b/src/test/kotlin/com/github/yeoli/devlog/domain/memo/domain/MemoTest.kt @@ -0,0 +1,58 @@ +package com.github.yeoli.devlog.domain.memo.domain + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class MemoTest { + + @Test + fun test_Memo_생성_성공() { + val memo = Memo( + content = "테스트 메모", + commitHash = "abc123", + filePath = "/path/SampleFile.kt", + selectedCodeSnippet = "val selected = 42", + selectionStart = 5, + selectionEnd = 10, + visibleStart = 1, + visibleEnd = 20 + ) + + assertEquals("테스트 메모", memo.content) + assertEquals("abc123", memo.commitHash) + assertEquals("/path/SampleFile.kt", memo.filePath) + assertEquals("val selected = 42", memo.selectedCodeSnippet) + assertEquals(5, memo.selectionStart) + assertEquals(10, memo.selectionEnd) + assertEquals(1, memo.visibleStart) + assertEquals(20, memo.visibleEnd) + assertTrue(memo.id > 0) + assertNotNull(memo.createdAt) + assertNotNull(memo.updatedAt) + } + + @Test + fun test_Memo_생성_실패_selection_범위() { + assertFailsWith { + Memo( + content = "잘못된 메모", + selectionStart = 10, + selectionEnd = 5 + ) + } + } + + @Test + fun test_Memo_생성_실패_visible_범위() { + assertFailsWith { + Memo( + content = "보이는 영역 오류", + visibleStart = 20, + visibleEnd = 10 + ) + } + } +} diff --git a/src/test/kotlin/com/github/yeoli/devlog/domain/memo/service/MemoServiceTest.kt b/src/test/kotlin/com/github/yeoli/devlog/domain/memo/service/MemoServiceTest.kt new file mode 100644 index 0000000..816b289 --- /dev/null +++ b/src/test/kotlin/com/github/yeoli/devlog/domain/memo/service/MemoServiceTest.kt @@ -0,0 +1,169 @@ +package com.github.yeoli.devlog.domain.memo.service + +import com.github.yeoli.devlog.domain.memo.domain.Memo +import com.intellij.mock.MockFileDocumentManagerImpl +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.editor.EditorFactory +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.util.Disposer +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.testFramework.replaceService +import com.intellij.util.Function +import java.awt.Point + +class MemoServiceTest : BasePlatformTestCase() { + + fun test_메모_생성_성공() { + // given + val psiFile = myFixture.configureByText( + "SampleFile.kt", + """ + fun main() { + val selected = 42 + println(selected) + } + """.trimIndent() + ) + val memoContent = "테스트 메모" + val editor = myFixture.editor + val document = editor.document + val targetSnippet = "val selected = 42" + val selectionStart = document.text.indexOf(targetSnippet) + assertTrue("선택할 코드 스니펫을 찾지 못했습니다.", selectionStart >= 0) + val selectionEnd = selectionStart + targetSnippet.length + editor.selectionModel.setSelection(selectionStart, selectionEnd) + + val visibleArea = editor.scrollingModel.visibleAreaOnScrollingFinished + val expectedVisibleStart = editor.xyToLogicalPosition(visibleArea.location).line + val expectedVisibleEnd = editor.xyToLogicalPosition( + Point(visibleArea.x, visibleArea.y + visibleArea.height) + ).line + + // when + val memo: Memo? = MemoService().createMemo(memoContent, project) + // then + if (memo != null) { + assertEquals(memoContent, memo.content) + assertEquals(targetSnippet, memo.selectedCodeSnippet) + assertEquals(psiFile.virtualFile.path, memo.filePath) + assertEquals(selectionStart, memo.selectionStart) + assertEquals(selectionEnd, memo.selectionEnd) + assertEquals(expectedVisibleStart, memo.visibleStart) + assertEquals(expectedVisibleEnd, memo.visibleEnd) + assertNull(memo.commitHash) + } else { + fail("memo가 null 입니다.") + } + + } + + fun test_메모_생성_선택없음() { + // given + val psiFile = myFixture.configureByText( + "SampleFile.kt", + """ + fun main() { + val selected = 42 + println(selected) + } + """.trimIndent() + ) + val memoContent = "선택 없음 메모" + val editor = myFixture.editor + val document = editor.document + val caretTarget = document.text.indexOf("println(selected)") + assertTrue("커서를 이동할 코드를 찾지 못했습니다.", caretTarget >= 0) + editor.caretModel.moveToOffset(caretTarget) + editor.selectionModel.removeSelection() + + val visibleArea = editor.scrollingModel.visibleAreaOnScrollingFinished + val expectedVisibleStart = editor.xyToLogicalPosition(visibleArea.location).line + val expectedVisibleEnd = editor.xyToLogicalPosition( + Point(visibleArea.x, visibleArea.y + visibleArea.height) + ).line + + // when + val memo: Memo? = MemoService().createMemo(memoContent, project) + // then + if (memo != null) { + assertEquals(memoContent, memo.content) + assertNull(memo.selectedCodeSnippet) + assertEquals(caretTarget, memo.selectionStart) + assertEquals(caretTarget, memo.selectionEnd) + assertEquals(psiFile.virtualFile.path, memo.filePath) + assertEquals(expectedVisibleStart, memo.visibleStart) + assertEquals(expectedVisibleEnd, memo.visibleEnd) + assertNull(memo.commitHash) + } else { + fail("memo가 null 입니다.") + } + } + + fun test_메모_생성_에디터없음_예외() { + // given + val psiFile = myFixture.configureByText( + "SampleFile.kt", + """ + fun main() {} + """.trimIndent() + ) + val fileEditorManager = FileEditorManager.getInstance(project) + fileEditorManager.closeFile(psiFile.virtualFile) + assertNull("선택된 에디터가 없어야 합니다.", fileEditorManager.selectedTextEditor) + + // expect + val memo: Memo? = MemoService().createMemo("에디터 없음", project) + assertNull(memo); + } + + fun test_메모_생성_파일경로없음() { + // given + myFixture.configureByText( + "SampleFile.kt", + """ + fun main() { + val selected = 42 + } + """.trimIndent() + ) + val memoContent = "파일 경로 없음" + val editor = myFixture.editor + val document = editor.document + val snippet = "val selected = 42" + val selectionStart = document.text.indexOf(snippet) + assertTrue("선택할 코드 스니펫을 찾지 못했습니다.", selectionStart >= 0) + val selectionEnd = selectionStart + snippet.length + editor.selectionModel.setSelection(selectionStart, selectionEnd) + + val mockDisposable = Disposer.newDisposable() + val mockFileDocumentManager = MockFileDocumentManagerImpl( + null, + Function { text -> EditorFactory.getInstance().createDocument(text) } + ) + ApplicationManager.getApplication().replaceService( + FileDocumentManager::class.java, + mockFileDocumentManager, + mockDisposable + ) + + try { + // when + val memo: Memo? = MemoService().createMemo(memoContent, project) + + // then + if (memo != null) { + assertEquals(memoContent, memo.content) + assertEquals(snippet, memo.selectedCodeSnippet) + assertNull("파일 경로가 null 이어야 합니다.", memo.filePath) + assertEquals(selectionStart, memo.selectionStart) + assertEquals(selectionEnd, memo.selectionEnd) + } else { + fail("memo가 null 입니다.") + } + + } finally { + Disposer.dispose(mockDisposable) + } + } +}