├── settings.gradle ├── .gitignore ├── gradle.properties ├── test ├── mockito-extensions │ └── org.mockito.plugins.MockMaker └── scratch │ ├── ScratchTests.kt │ ├── test-util.kt │ ├── ScratchConfigTests.kt │ └── MrScratchManagerTests.kt ├── scratch.jar ├── screenshot.png ├── old_screenshot.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src └── scratch │ ├── Answer.kt │ ├── Scratch.kt │ ├── ide │ ├── popup │ │ ├── ScratchListPopupStep.kt │ │ ├── ScratchListElementRenderer.kt │ │ ├── PopupModelWithMovableItems.kt │ │ └── ScratchListPopup.kt │ ├── ScratchLog.kt │ ├── OpenEditorTracker.kt │ ├── ScratchComponent.kt │ ├── Util.kt │ ├── ScratchConfigPersistence.kt │ ├── ClipboardListener.kt │ ├── FileSystem.kt │ ├── Actions.kt │ └── Ide.kt │ ├── ScratchConfig.kt │ └── MrScratchManager.kt ├── .github └── workflows │ └── gradle.yml ├── gradlew.bat ├── README.md ├── resources └── META-INF │ └── plugin.xml └── gradlew /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = "scratch" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .idea 3 | build 4 | out 5 | *.iml -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.stdlib.default.dependency = false 2 | -------------------------------------------------------------------------------- /test/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline -------------------------------------------------------------------------------- /scratch.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkandalov/scratch/HEAD/scratch.jar -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkandalov/scratch/HEAD/screenshot.png -------------------------------------------------------------------------------- /old_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkandalov/scratch/HEAD/old_screenshot.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkandalov/scratch/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/scratch/Answer.kt: -------------------------------------------------------------------------------- 1 | package scratch 2 | 3 | 4 | data class Answer(val isYes: Boolean, val explanation: String? = null) { 5 | val isNo = !isYes 6 | 7 | override fun toString() = if (isYes) "Yes" else "No($explanation)" 8 | 9 | companion object { 10 | fun no(explanation: String) = Answer(false, explanation) 11 | fun yes() = Answer(true) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-20.04 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-java@v3 13 | with: 14 | distribution: 'adopt-hotspot' 15 | java-version: '17' 16 | - name: Gradle 17 | run: | 18 | chmod +x gradlew 19 | ./gradlew check -------------------------------------------------------------------------------- /src/scratch/Scratch.kt: -------------------------------------------------------------------------------- 1 | package scratch 2 | 3 | 4 | data class Scratch(val fullNameWithMnemonics: String) { 5 | val name: String = fullNameWithMnemonics.extractName() 6 | val extension: String = fullNameWithMnemonics.extractExtension() 7 | val fileName: String get() = "$name.$extension" 8 | 9 | override fun toString() = "{fullNameWithMnemonics='$fullNameWithMnemonics'}" 10 | 11 | private fun String.extractExtension(): String { 12 | val index = lastIndexOf(".") 13 | return if (index == -1) "" else substring(index + 1).replace("&", "") 14 | } 15 | 16 | private fun String.extractName(): String { 17 | var index = lastIndexOf(".") 18 | if (index == -1) index = length 19 | return substring(0, index).replace("&", "") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/scratch/ScratchTests.kt: -------------------------------------------------------------------------------- 1 | package scratch 2 | 3 | import org.hamcrest.MatcherAssert.assertThat 4 | import org.hamcrest.core.IsEqual.equalTo 5 | import org.junit.Test 6 | 7 | class ScratchTests { 8 | @Test fun `creating scratches`() { 9 | Scratch("scratch.txt").apply { 10 | assertThat(name, equalTo("scratch")) 11 | assertThat(extension, equalTo("txt")) 12 | } 13 | 14 | Scratch("&scratch.txt").apply { 15 | assertThat(name, equalTo("scratch")) 16 | assertThat(extension, equalTo("txt")) 17 | } 18 | 19 | Scratch("scratch.t&xt").apply { 20 | assertThat(name, equalTo("scratch")) 21 | assertThat(extension, equalTo("txt")) 22 | } 23 | 24 | Scratch("scratch").apply { 25 | assertThat(name, equalTo("scratch")) 26 | assertThat(extension, equalTo("")) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/scratch/test-util.kt: -------------------------------------------------------------------------------- 1 | package scratch 2 | 3 | import org.mockito.internal.matchers.Equals 4 | import org.mockito.internal.matchers.InstanceOf 5 | import org.mockito.internal.matchers.Same 6 | import org.mockito.internal.progress.ThreadSafeMockingProgress 7 | import kotlin.reflect.KClass 8 | 9 | fun eq(value: T): T { 10 | ThreadSafeMockingProgress.mockingProgress().argumentMatcherStorage.reportMatcher(Equals(value)) 11 | return value 12 | } 13 | 14 | fun same(value: T): T { 15 | ThreadSafeMockingProgress.mockingProgress().argumentMatcherStorage.reportMatcher(Same(value)) 16 | return value 17 | } 18 | 19 | fun some(kClass: KClass): T { 20 | ThreadSafeMockingProgress.mockingProgress().argumentMatcherStorage 21 | .reportMatcher(InstanceOf(kClass.java, "")) 22 | return when { 23 | kClass.java.isEnum -> kClass.java.enumConstants.first() 24 | else -> kClass.constructors.find { it.parameters.isEmpty() }!!.call() 25 | } 26 | } -------------------------------------------------------------------------------- /test/scratch/ScratchConfigTests.kt: -------------------------------------------------------------------------------- 1 | package scratch 2 | 3 | import org.hamcrest.MatcherAssert.assertThat 4 | import org.hamcrest.core.IsEqual.equalTo 5 | import org.junit.Test 6 | import scratch.ScratchConfig.Companion.defaultConfig 7 | import scratch.ScratchConfig.Companion.down 8 | import scratch.ScratchConfig.Companion.up 9 | 10 | class ScratchConfigTests { 11 | private val scratch1 = Scratch("scratch1.txt") 12 | private val scratch2 = Scratch("scratch2.txt") 13 | private val scratch3 = Scratch("scratch3.txt") 14 | private val config = defaultConfig.with(listOf(scratch1, scratch2, scratch3)) 15 | 16 | @Test fun `moving top scratch to bottom`() { 17 | assertThat(config.move(scratch1, up), equalTo(defaultConfig.with(listOf(scratch2, scratch3, scratch1)))) 18 | } 19 | 20 | @Test fun `moving bottom scratch to top`() { 21 | assertThat(config.move(scratch3, down), equalTo(defaultConfig.with(listOf(scratch3, scratch1, scratch2)))) 22 | } 23 | 24 | @Test fun `moving scratch up`() { 25 | assertThat(config.move(scratch2, up), equalTo(defaultConfig.with(listOf(scratch2, scratch1, scratch3)))) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/scratch/ide/popup/ScratchListPopupStep.kt: -------------------------------------------------------------------------------- 1 | package scratch.ide.popup 2 | 3 | import com.intellij.openapi.fileTypes.FileTypeManager 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.openapi.ui.popup.PopupStep 6 | import com.intellij.openapi.ui.popup.util.BaseListPopupStep 7 | import scratch.Scratch 8 | import scratch.ide.ScratchComponent.Companion.mrScratchManager 9 | import scratch.ide.wrapAsDataHolder 10 | 11 | 12 | class ScratchListPopupStep(scratches: List, private val project: Project): BaseListPopupStep("List of Scratches", scratches) { 13 | private val fileTypeManager = FileTypeManager.getInstance() 14 | 15 | override fun onChosen(scratch: Scratch, finalChoice: Boolean): PopupStep<*>? { 16 | if (!finalChoice) return null 17 | mrScratchManager().userWantsToOpenScratch(scratch, project.wrapAsDataHolder()) 18 | return PopupStep.FINAL_CHOICE 19 | } 20 | 21 | override fun getTextFor(scratch: Scratch) = scratch.fullNameWithMnemonics 22 | 23 | override fun getIconFor(scratch: Scratch) = fileTypeManager.getFileTypeByExtension(scratch.extension).icon 24 | 25 | override fun isMnemonicsNavigationEnabled() = true 26 | 27 | override fun isSpeedSearchEnabled() = true 28 | 29 | override fun isAutoSelectionEnabled() = false 30 | } 31 | -------------------------------------------------------------------------------- /src/scratch/ide/ScratchLog.kt: -------------------------------------------------------------------------------- 1 | package scratch.ide 2 | 3 | import com.intellij.notification.NotificationType.INFORMATION 4 | import com.intellij.notification.NotificationType.WARNING 5 | import com.intellij.openapi.diagnostic.Logger 6 | import scratch.Scratch 7 | 8 | 9 | class ScratchLog { 10 | 11 | fun failedToRename(scratch: Scratch) { 12 | showNotification("Failed to rename scratch: ${scratch.fileName}", WARNING) 13 | } 14 | 15 | fun listeningToClipboard(isListening: Boolean) = 16 | if (isListening) showNotification("Started listening to clipboard", INFORMATION) 17 | else showNotification("Stopped listening to clipboard", INFORMATION) 18 | 19 | fun failedToOpenDefaultScratch() = showNotification("Failed to open default scratch", WARNING) 20 | 21 | fun failedToOpen(scratch: Scratch) = showNotification("Failed to open scratch: '${scratch.fileName}'", WARNING) 22 | 23 | fun failedToCreate(scratch: Scratch) = showNotification("Failed to create scratch: '${scratch.fileName}'", WARNING) 24 | 25 | fun failedToDelete(scratch: Scratch) = showNotification("Failed to delete scratch: '${scratch.fileName}'", WARNING) 26 | 27 | fun failedToFindVirtualFileFor(scratch: Scratch) = log.warn("Failed to find virtual file for '${scratch.fileName}'") 28 | 29 | companion object { 30 | private val log = Logger.getInstance(ScratchLog::class.java) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/scratch/ide/OpenEditorTracker.kt: -------------------------------------------------------------------------------- 1 | package scratch.ide 2 | 3 | import com.intellij.openapi.application.Application 4 | import com.intellij.openapi.application.ApplicationManager 5 | import com.intellij.openapi.fileEditor.FileEditorManagerEvent 6 | import com.intellij.openapi.fileEditor.FileEditorManagerListener 7 | import com.intellij.openapi.fileEditor.FileEditorManagerListener.FILE_EDITOR_MANAGER 8 | import com.intellij.openapi.project.Project 9 | import com.intellij.openapi.project.ProjectManager 10 | import com.intellij.openapi.project.ProjectManagerListener 11 | import scratch.MrScratchManager 12 | 13 | class OpenEditorTracker( 14 | private val mrScratchManager: MrScratchManager, 15 | private val fileSystem: FileSystem, 16 | private val application: Application = ApplicationManager.getApplication() 17 | ) { 18 | fun startTracking() { 19 | val fileEditorListener = object: FileEditorManagerListener { 20 | override fun selectionChanged(event: FileEditorManagerEvent) { 21 | val virtualFile = event.newFile ?: return 22 | if (fileSystem.isScratch(virtualFile)) { 23 | mrScratchManager.userOpenedScratch(virtualFile.name) 24 | } 25 | } 26 | } 27 | application.messageBus.connect().subscribe(ProjectManager.TOPIC, object: ProjectManagerListener { 28 | override fun projectOpened(project: Project) { 29 | project.messageBus.connect(project).subscribe(FILE_EDITOR_MANAGER, fileEditorListener) 30 | } 31 | }) 32 | } 33 | } -------------------------------------------------------------------------------- /src/scratch/ide/ScratchComponent.kt: -------------------------------------------------------------------------------- 1 | package scratch.ide 2 | 3 | import com.intellij.ide.scratch.ScratchFileService 4 | import com.intellij.ide.scratch.ScratchRootType 5 | import com.intellij.notification.NotificationType.INFORMATION 6 | import com.intellij.openapi.application.ApplicationManager.getApplication 7 | import com.intellij.openapi.fileEditor.impl.NonProjectFileWritingAccessExtension 8 | import com.intellij.openapi.util.io.FileUtil 9 | import com.intellij.openapi.vfs.VirtualFile 10 | import com.intellij.openapi.vfs.VirtualFileManager 11 | import scratch.MrScratchManager 12 | import java.io.File 13 | 14 | class ScratchComponent { 15 | private val log = ScratchLog() 16 | private val configPersistence = ScratchConfigPersistence.instance 17 | private val fileSystem = FileSystem(configPersistence.scratchesFolderPath) 18 | private val mrScratchManager = MrScratchManager(Ide(fileSystem, log), fileSystem, configPersistence.toScratchConfig(), log) 19 | 20 | init { 21 | mrScratchManager.syncScratchesWithFileSystem() 22 | 23 | OpenEditorTracker(mrScratchManager, fileSystem).startTracking() 24 | ClipboardListener(mrScratchManager).startListening() 25 | 26 | if (configPersistence.listenToClipboard) log.listeningToClipboard(true) 27 | } 28 | 29 | companion object { 30 | fun mrScratchManager() = getApplication().getComponent(ScratchComponent::class.java).mrScratchManager 31 | fun fileSystem() = getApplication().getComponent(ScratchComponent::class.java).fileSystem 32 | } 33 | } 34 | 35 | class FileWritingAccessExtension: NonProjectFileWritingAccessExtension { 36 | override fun isWritable(virtualFile: VirtualFile) = ScratchComponent.fileSystem().isScratch(virtualFile) 37 | } 38 | -------------------------------------------------------------------------------- /src/scratch/ide/Util.kt: -------------------------------------------------------------------------------- 1 | package scratch.ide 2 | 3 | import com.intellij.notification.Notification 4 | import com.intellij.notification.NotificationListener 5 | import com.intellij.notification.NotificationType 6 | import com.intellij.notification.Notifications 7 | import com.intellij.openapi.Disposable 8 | import com.intellij.openapi.application.ApplicationManager 9 | import com.intellij.openapi.command.CommandProcessor 10 | import com.intellij.openapi.command.UndoConfirmationPolicy.DO_NOT_REQUEST_CONFIRMATION 11 | import com.intellij.openapi.project.Project 12 | import com.intellij.openapi.util.Disposer 13 | import com.intellij.openapi.util.Key 14 | import com.intellij.openapi.util.UserDataHolder 15 | import com.intellij.openapi.util.UserDataHolderBase 16 | 17 | private val projectKey = Key.create("Project") 18 | 19 | fun Project?.wrapAsDataHolder(): UserDataHolder = UserDataHolderBase().apply { 20 | putUserData(projectKey, this@wrapAsDataHolder) 21 | } 22 | 23 | fun UserDataHolder.extractProject(): Project = getUserData(projectKey)!! 24 | 25 | fun CommandProcessor.execute(f: () -> Unit) { 26 | executeCommand(null, f, null, null, DO_NOT_REQUEST_CONFIRMATION) 27 | } 28 | 29 | fun showNotification(message: String, notificationType: NotificationType, listener: () -> Unit = {}) { 30 | val title = "Scratch plugin" 31 | val notificationListener = NotificationListener { notification, _ -> 32 | listener.invoke() 33 | notification.expire() 34 | } 35 | val notification = Notification(title, title, message, notificationType, notificationListener) 36 | 37 | ApplicationManager.getApplication() 38 | .messageBus.syncPublisher(Notifications.TOPIC) 39 | .notify(notification) 40 | } 41 | 42 | fun Disposable.whenDisposed(f: () -> Unit) { 43 | // Always register new instance of Disposable because there is one-to-one relationship between parent and child disposables. 44 | Disposer.register(this, Disposable { f() }) 45 | } 46 | -------------------------------------------------------------------------------- /src/scratch/ide/ScratchConfigPersistence.kt: -------------------------------------------------------------------------------- 1 | package scratch.ide 2 | 3 | import com.intellij.ide.scratch.ScratchFileService 4 | import com.intellij.ide.scratch.ScratchRootType 5 | import com.intellij.openapi.components.PersistentStateComponent 6 | import com.intellij.openapi.components.ServiceManager 7 | import com.intellij.openapi.components.State 8 | import com.intellij.openapi.components.Storage 9 | import com.intellij.util.xmlb.XmlSerializerUtil 10 | import com.intellij.util.xmlb.annotations.OptionTag 11 | import scratch.Scratch 12 | import scratch.ScratchConfig 13 | import scratch.ScratchConfig.AppendType 14 | import scratch.ScratchConfig.DefaultScratchMeaning 15 | 16 | 17 | @State(name = "ScratchConfig", storages = [Storage("scratch_config.xml")]) 18 | data class ScratchConfigPersistence( 19 | @OptionTag(valueAttribute = "isListenToClipboard") 20 | var listenToClipboard: Boolean = false, 21 | var fullScratchNamesOrdered: ArrayList = ArrayList(), // This MUST BE an ArrayList for IJ serialization to work. 22 | var lastOpenedScratch: String? = null, 23 | var clipboardAppendType: AppendType? = null, 24 | var newScratchAppendType: AppendType? = null, 25 | var defaultScratchMeaning: DefaultScratchMeaning? = null, 26 | var scratchesFolderPath: String? = ScratchFileService.getInstance().getRootPath(ScratchRootType.getInstance()) 27 | ) : PersistentStateComponent { 28 | 29 | fun toScratchConfig() = 30 | ScratchConfig.defaultConfig 31 | .listenToClipboard(listenToClipboard) 32 | .with(fullScratchNamesOrdered.map { Scratch(it) }) 33 | .withLastOpenedScratch(if (lastOpenedScratch == null) null else Scratch(lastOpenedScratch!!)) 34 | .withDefaultScratchMeaning(defaultScratchMeaning) 35 | .withClipboard(clipboardAppendType) 36 | .withNewScratch(newScratchAppendType) 37 | 38 | fun updateFrom(config: ScratchConfig) { 39 | listenToClipboard = config.listenToClipboard 40 | fullScratchNamesOrdered = ArrayList(config.scratches.map { it.fullNameWithMnemonics }) 41 | lastOpenedScratch = config.lastOpenedScratch?.fullNameWithMnemonics 42 | defaultScratchMeaning = config.defaultScratchMeaning 43 | } 44 | 45 | override fun getState() = this 46 | 47 | override fun loadState(state: ScratchConfigPersistence) = XmlSerializerUtil.copyBean(state, this) 48 | 49 | companion object { 50 | val instance: ScratchConfigPersistence 51 | get() = ServiceManager.getService(ScratchConfigPersistence::class.java) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/scratch/ide/ClipboardListener.kt: -------------------------------------------------------------------------------- 1 | package scratch.ide 2 | 3 | import com.intellij.openapi.application.Application 4 | import com.intellij.openapi.application.ApplicationManager 5 | import com.intellij.openapi.command.CommandProcessor 6 | import com.intellij.openapi.diagnostic.Logger 7 | import com.intellij.openapi.ide.CopyPasteManager 8 | import com.intellij.openapi.ide.CopyPasteManager.ContentChangedListener 9 | import com.intellij.openapi.ide.CopyPasteManager.getInstance 10 | import scratch.MrScratchManager 11 | import java.awt.datatransfer.DataFlavor.stringFlavor 12 | import java.awt.datatransfer.Transferable 13 | import java.awt.datatransfer.UnsupportedFlavorException 14 | import java.io.IOException 15 | 16 | /** 17 | * Note that it's possible to turn on clipboard listener and forget about it. 18 | * So it will keep appending clipboard content to default scratch forever. 19 | * To avoid the problem remind user on IDE startup that clipboard listener is on. 20 | */ 21 | class ClipboardListener( 22 | private val mrScratchManager: MrScratchManager, 23 | private val copyPasteManager: CopyPasteManager = getInstance(), 24 | private val commandProcessor: CommandProcessor = CommandProcessor.getInstance(), 25 | private val application: Application = ApplicationManager.getApplication() 26 | ) { 27 | fun startListening() { 28 | val listener = ContentChangedListener { oldTransferable: Transferable?, newTransferable: Transferable? -> 29 | if (mrScratchManager.shouldListenToClipboard()) { 30 | try { 31 | pasteIntoDefaultScratch(newTransferable, oldTransferable) 32 | } catch (e: UnsupportedFlavorException) { 33 | log.info(e) 34 | } catch (e: IOException) { 35 | log.info(e) 36 | } 37 | } 38 | } 39 | copyPasteManager.addContentChangedListener(listener, application) 40 | } 41 | 42 | private fun pasteIntoDefaultScratch(newTransferable: Transferable?, oldTransferable: Transferable?) { 43 | val oldClipboard = oldTransferable?.getTransferData(stringFlavor)?.toString() 44 | val clipboard = newTransferable?.getTransferData(stringFlavor)?.toString() 45 | 46 | if (clipboard != null && oldClipboard != clipboard) { 47 | // Invoke action later so that modification of document is not tracked by IDE as undoable "Copy" action 48 | // See https://github.com/dkandalov/scratch/issues/30 49 | application.invokeLater { 50 | commandProcessor.execute { 51 | application.runWriteAction { 52 | mrScratchManager.clipboardListenerWantsToPasteTextToScratch(clipboard) 53 | } 54 | } 55 | } 56 | } 57 | } 58 | 59 | companion object { 60 | private val log = Logger.getInstance(ClipboardListener::class.java) 61 | } 62 | } -------------------------------------------------------------------------------- /src/scratch/ide/popup/ScratchListElementRenderer.kt: -------------------------------------------------------------------------------- 1 | package scratch.ide.popup 2 | 3 | import com.intellij.openapi.ui.popup.ListItemDescriptor 4 | import com.intellij.openapi.util.IconLoader 5 | import com.intellij.ui.ColorUtil 6 | import com.intellij.ui.popup.list.GroupedItemsListRenderer 7 | import com.intellij.util.ui.UIUtil 8 | import scratch.Scratch 9 | import javax.swing.JList 10 | 11 | fun createListItemDescriptor(myPopup: ScratchListPopup) = object: ListItemDescriptor { 12 | override fun getTextFor(value: Scratch) = myPopup.listStep.getTextFor(value) 13 | override fun getTooltipFor(value: Scratch): String? = null 14 | override fun getIconFor(value: Scratch) = myPopup.listStep.getIconFor(value) 15 | override fun hasSeparatorAboveOf(value: Scratch) = myPopup.listModel.isSeparatorAboveOf(value) 16 | override fun getCaptionAboveOf(value: Scratch) = myPopup.listModel.getCaptionAboveOf(value) 17 | } 18 | 19 | internal class ScratchListElementRenderer( 20 | private val myPopup: ScratchListPopup 21 | ): GroupedItemsListRenderer(createListItemDescriptor(myPopup)) { 22 | 23 | override fun customizeComponent(list: JList, value: Scratch, isSelected: Boolean) { 24 | val step = myPopup.listStep 25 | val isSelectable = step.isSelectable(value) 26 | myTextLabel.isEnabled = isSelectable 27 | 28 | if (step.isMnemonicsNavigationEnabled) { 29 | val navigationFilter = step.mnemonicNavigationFilter 30 | val pos = navigationFilter?.getMnemonicPos(value) ?: -1 31 | if (pos != -1) { 32 | var text = myTextLabel.text 33 | text = text.substring(0, pos) + text.substring(pos + 1) 34 | myTextLabel.text = text 35 | myTextLabel.displayedMnemonicIndex = pos 36 | } 37 | } else { 38 | myTextLabel.displayedMnemonicIndex = -1 39 | } 40 | 41 | if (step.hasSubstep(value) && isSelectable) { 42 | myNextStepLabel.isVisible = true 43 | myNextStepLabel.icon = if (isSelected) { 44 | val isDark = ColorUtil.isDark(UIUtil.getListSelectionBackground(true)) 45 | if (isDark) nextStepInverted else nextStep 46 | } else { 47 | nextStepGrayed 48 | } 49 | } else { 50 | myNextStepLabel.isVisible = false 51 | //myNextStepLabel.setIcon(PopupIcons.EMPTY_ICON); 52 | } 53 | 54 | if (isSelected) { 55 | setSelected(myNextStepLabel) 56 | } else { 57 | setDeselected(myNextStepLabel) 58 | } 59 | } 60 | 61 | companion object { 62 | val nextStep = IconLoader.getIcon("/icons/ide/nextStep.svg", IconLoader::class.java) 63 | val nextStepGrayed = IconLoader.getIcon("/icons/ide/nextStep_dark.png", IconLoader::class.java) 64 | val nextStepInverted = IconLoader.getIcon("/icons/ide/nextStepInverted.svg", IconLoader::class.java) 65 | } 66 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if %ERRORLEVEL% equ 0 goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if %ERRORLEVEL% equ 0 goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | set EXIT_CODE=%ERRORLEVEL% 84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 86 | exit /b %EXIT_CODE% 87 | 88 | :mainEnd 89 | if "%OS%"=="Windows_NT" endlocal 90 | 91 | :omega 92 | -------------------------------------------------------------------------------- /src/scratch/ScratchConfig.kt: -------------------------------------------------------------------------------- 1 | package scratch 2 | 3 | import scratch.ScratchConfig.AppendType.APPEND 4 | import scratch.ScratchConfig.AppendType.PREPEND 5 | import scratch.ScratchConfig.DefaultScratchMeaning.TOPMOST 6 | 7 | 8 | data class ScratchConfig( 9 | val scratches: List, 10 | val lastOpenedScratch: Scratch?, 11 | val listenToClipboard: Boolean, 12 | val clipboardAppendType: AppendType, 13 | private val newScratchAppendType: AppendType, 14 | val defaultScratchMeaning: DefaultScratchMeaning 15 | ) { 16 | enum class AppendType { 17 | APPEND, PREPEND 18 | } 19 | 20 | enum class DefaultScratchMeaning { 21 | TOPMOST, LAST_OPENED 22 | } 23 | 24 | fun add(scratch: Scratch): ScratchConfig { 25 | val newScratches = scratches.toMutableList() 26 | when (newScratchAppendType) { 27 | APPEND -> newScratches.add(scratch) 28 | PREPEND -> newScratches.add(0, scratch) 29 | } 30 | return this.with(newScratches) 31 | } 32 | 33 | fun with(newScratches: List) = copy(scratches = newScratches) 34 | 35 | fun without(scratch: Scratch) = copy(scratches = scratches.filter { it != scratch }) 36 | 37 | fun replace(scratch: Scratch, newScratch: Scratch) = copy( 38 | scratches = scratches.map { if (it == scratch) newScratch else it }, 39 | lastOpenedScratch = if (scratch == lastOpenedScratch) newScratch else lastOpenedScratch 40 | ) 41 | 42 | fun move(scratch: Scratch, shift: Int): ScratchConfig { 43 | val oldIndex = scratches.indexOf(scratch) 44 | var newIndex = oldIndex + shift 45 | if (newIndex < 0) newIndex += scratches.size 46 | if (newIndex >= scratches.size) newIndex -= scratches.size 47 | 48 | val newScratches = scratches.toMutableList() 49 | newScratches.removeAt(oldIndex) 50 | newScratches.add(newIndex, scratch) 51 | return this.with(newScratches) 52 | } 53 | 54 | fun listenToClipboard(value: Boolean) = copy(listenToClipboard = value) 55 | 56 | fun withClipboard(value: AppendType?): ScratchConfig { 57 | if (value == null) return this 58 | return copy(clipboardAppendType = value) 59 | } 60 | 61 | fun withNewScratch(value: AppendType?): ScratchConfig { 62 | if (value == null) return this 63 | return copy(newScratchAppendType = value) 64 | } 65 | 66 | fun withDefaultScratchMeaning(value: DefaultScratchMeaning?): ScratchConfig { 67 | return if (value == null) this 68 | else copy(defaultScratchMeaning = value) 69 | } 70 | 71 | fun withLastOpenedScratch(value: Scratch?) = copy(lastOpenedScratch = value) 72 | 73 | companion object { 74 | val defaultConfig = ScratchConfig( 75 | scratches = emptyList(), 76 | lastOpenedScratch = null, 77 | listenToClipboard = false, 78 | clipboardAppendType = APPEND, 79 | newScratchAppendType = APPEND, 80 | defaultScratchMeaning = TOPMOST 81 | ) 82 | const val up = -1 83 | const val down = 1 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/dkandalov/scratch/workflows/CI/badge.svg)](https://github.com/dkandalov/scratch/actions) 2 | 3 | screenshot 4 | 5 | ### Scratch Plugin 6 | 7 | This is a plugin for IntelliJ IDEs for cases when you want to type something without having to create new file or switch to another editor. 8 | 9 | It allows you to have multiple temporary editor tabs, one of which can be made the default. 10 | The default scratch tab can be quickly opened with a shortcut. 11 | 12 | Contents of the temporary editor tabs are stored outside of the project, so your directory will not get cluttered. 13 | 14 | Note that since IJ14 there are [built-in scratches](https://blog.jetbrains.com/idea/2014/09/intellij-idea-14-eap-138-2210-brings-scratch-files-and-better-mercurial-integration/) 15 | with similar functionality which unfortunately still don't have actions to open "default" scratch and show list of available scratches. 16 | At some point this plugin might be reimplemented as additional actions on top of built-in scratches. 17 | 18 | 19 | ### How to use? 20 | - `Alt+C, Alt+C` - open default scratch 21 | (it can be 'last opened' or 'topmost in scratches list'; see `Tools -> Scratch -> Default Scratch`) 22 | - `Alt+C, Alt+S` - open list with all scratches 23 | - `Alt+C, Alt+A` - add new scratch 24 | 25 | In scratches list popup: 26 | - `Alt+Insert`* - add new scratch 27 | - `Alt+Up/Down` - move scratch 28 | - `Shift+F6`* - rename scratch 29 | - `Delete`* - delete scratch 30 | - `Ctrl+Delete` - delete scratch without prompt 31 | 32 | \* - shortcuts are copied from `Delete`, `Rename` and `Generate` actions, 33 | i.e. if you have changed keyboard layout, your own shortcuts should work as well. 34 | 35 | Some of these actions are also in `Tools -> Scratch` menu. 36 | 37 | 38 | ### "Hidden" features 39 | - **Listen to clipboard and add its content to default scratch**.
40 | In `IDE Settings -> Keymap` search for "Listen to clipboard" and assign it a shortcut (`Alt+C, Alt+V` is recommended :)). 41 | (Note that you can do similar thing with built-in IDE clipboard. 42 | E.g. `Ctrl+C` several words, `Ctrl+Shift+V` to see clipboard history, select several items. 43 | If you press `Ctrl+C`, selected items will be joined with new lines.) 44 | 45 | - **Listen to clipboard append/prepend** (default is "APPEND").
46 | Find folder with IDE preference files (e.g. `$HOME/.IntelliJ/options` on Windows/Linux; `$HOME/Library/Preferences/IntelliJIdea/options` on OSX).
47 | Edit `scratch_config.xml` to add the following line (works after IDE restart): 48 | ``` 49 |