Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ local.properties
.env.*
!.env.example

# Local AI assistant files
.codex/
.claude/
.cursor/
.windsurf/
.continue/
.aider*
.ai/

secrets/
.codexp-signing/
*.pem
Expand Down
34 changes: 34 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,40 @@ Before opening a pull request:
- Update `CHANGELOG.md` when the change affects users, compatibility, build, or release behavior.
- Do not include private certificates, Marketplace tokens, or local signing files.

## Kotlin Documentation

Use KDoc for Kotlin documentation comments. KDoc comments use `/** ... */` and should be attached directly to declarations such as classes, interfaces, objects, functions, properties, and enum entries.

Write KDoc when it explains a public or internal contract, a non-obvious domain rule, lifecycle behavior, persistence compatibility, threading expectations, or an extension point. Avoid comments that only repeat the declaration name or restate obvious implementation details.

Recommended format:

```kotlin
/**
* Records an IDE event and updates user progress.
*
* The returned result describes every UI notification that should be emitted
* after the state mutation is complete.
*
* @param state Persistent CodeXP state to mutate.
* @param event IDE event to record.
* @return Progress changes produced by the event.
*/
fun recordEvent(state: CodeXPState, event: Event): CodeXPProgressResult
```

KDoc guidelines:

- Start with a short summary sentence.
- Add a blank line before longer details.
- Use `@param` for parameters whose meaning is not obvious from the name.
- Use `@return` when the returned value carries meaningful behavior or state.
- Use `@property` for primary-constructor properties when documenting data classes.
- Use KDoc links like `[CodeXPState]`, `[CodeXPService]`, or `[Event.TYPING]` when referencing code symbols.
- Prefer documenting why a rule exists, what callers can rely on, and what must remain compatible.
- Keep implementation comments rare; when a block needs explanation, prefer extracting a named function and documenting that function with KDoc.
- Do not use KDoc to preserve stale history. Update or remove comments when behavior changes.

## Branch Workflow

- Use `develop` as the integration branch for normal work.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ The CodeXP plugin is designed to gamify your coding experience in IntelliJ IDEs.
- Other actions
- Provides periodic challenges to keep the coding experience engaging.
- Displays your level based on the accumulated xp, providing a fun and engaging way to track your coding activities.

Starting with version 2.1.0, CodeXP is being improved with AI-assisted development. This means the project will gradually refine its internal architecture and prepare for future features such as richer effects, configurable XP rules, cross-IDE progress continuity, and account-based synchronization.
<!-- Plugin description end -->

<br>
Expand Down
169 changes: 169 additions & 0 deletions docs/release-process.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# Release Process

CodeXP uses `develop` for day-to-day work and `main` for stable, release-ready code.

## Branch Model

- `main` is the stable branch that represents published or release-ready code.
- `develop` is the integration branch for normal work.
- Feature, fix, documentation, maintenance, and dependency branches should target `develop`.
- `main` should receive changes through pull requests from `develop` when those changes are ready to be part of the stable branch.
- Hotfixes may branch from `main`, merge back into `main`, and then be reconciled with `develop`.

## Branch Naming

Use short, descriptive branch names with one of these prefixes:

- `feature/` for user-facing features.
- `fix/` for bug fixes.
- `docs/` for documentation-only changes.
- `chore/` for maintenance, build, dependency, or repository hygiene changes.
- `release/` for release preparation branches.
- `hotfix/` for urgent fixes based on `main`.

Prefer lowercase kebab-case after the prefix:

```text
feature/custom-xp-rules
fix/idea-compatibility
docs/release-process
chore/update-github-actions
release/2.2.0
hotfix/2.1.1
```

## Pull Request Flow

Normal development:

```text
feature/* -> develop
fix/* -> develop
docs/* -> develop
chore/* -> develop
dependabot/* -> develop
```

Stable branch sync:

```text
develop -> main
```

Use `develop -> main` for release preparation or stable maintenance syncs. A stable maintenance sync should not imply Marketplace publishing.

## Pull Request Format

Use concise titles that describe the purpose of the change:

```text
feat: Add custom XP rules
fix: Restore compatibility with newer IDE builds
docs: Update release process
chore: Update GitHub Actions dependencies
Release CodeXP v2.2.0
```

For normal work, keep the body short:

```md
Briefly explain why this change is needed.

## Changes
- Add the main change.
- Note compatibility or maintenance impact when relevant.

Please check the build workflows pass before merging.
```

For `develop -> main` release PRs, use the existing release style:

```md
## Description
This PR merges the `develop` branch into `main` for the release of version X.Y.Z

<br>

## Features
- User-facing feature or compatibility change.

<br>

## Fixed
- User-facing bug fix.

<br>

Please check the build workflows pass before merging.
```

For stable maintenance syncs that are not plugin releases, state that clearly in the PR body.

## CI Expectations

The required CI job is `Build, test, and verify`.

It can take several minutes because it builds the plugin and runs IntelliJ Plugin Verifier across multiple IDE versions.

When waiting on a pull request:

- Wait for `Build, test, and verify` to complete before merging.
- If a branch is out of date with `main`, update the branch and wait for CI again.
- Cancelled runs can be normal when newer pushes supersede older runs on the same branch.
- A stable maintenance sync to `main` should not create a release draft or publish the plugin.

## Release Flow

Normal merges do not publish the plugin.

1. Merge release-ready changes into `main`.
2. Run and verify CI on `main`.
3. A maintainer prepares the release notes and GitHub Release.
4. Publishing is performed only by a maintainer.
5. Confirm the version is listed on JetBrains Marketplace.

Release draft creation must be an explicit maintainer action.

## Version And Compatibility Checks

Before publishing a release, verify:

- `pluginVersion` has the intended version.
- `pluginSinceBuild` and `pluginUntilBuild` match the intended IDE compatibility range.
- `./gradlew check koverXmlReport buildPlugin verifyPlugin` passes.

## Release Notes

Release notes are for plugin users. They should explain what changed from the user's perspective.

Include:

- IDE compatibility changes.
- User-visible features.
- Bug fixes.
- Stability, performance, installation, or behavior changes that affect users.
- Migration or update notes that affect users.

Exclude or minimize:

- CI, branch protection, or repository policy changes.
- Dependency maintenance that does not affect users.
- Contributor-only docs, templates, CODEOWNERS, and internal tests.
- Build tool updates unless they change compatibility, installation, or runtime behavior.

Example user-facing release note:

```md
### Changed
- Added support for IntelliJ IDEA 2025.2 and newer IDE builds up to 2026.1.

### Fixed
- Fixed compatibility issues that could prevent the plugin from loading on newer IntelliJ IDE versions.
```

GitHub Release drafts should use:

- Title: `vX.Y.Z`
- Tag: `vX.Y.Z`
- Target: the intended `main` commit
- Notes: user-facing release notes only
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package com.github.ilovegamecoding.intellijcodexp.activities

import com.github.ilovegamecoding.intellijcodexp.managers.CodeXPUIManager
import com.github.ilovegamecoding.intellijcodexp.services.CodeXPService
import com.intellij.openapi.application.ApplicationManager
import com.github.ilovegamecoding.intellijcodexp.presentation.overlay.CodeXPOverlayController
import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.StartupActivity
import com.intellij.openapi.startup.ProjectActivity

class CodeXPStartupActivity : StartupActivity {
override fun runActivity(project: Project) {
ApplicationManager.getApplication().getService(CodeXPService::class.java)
CodeXPUIManager.createDialogArea()
/**
* Initializes project-window CodeXP UI controllers after a project opens.
*/
class CodeXPStartupActivity : ProjectActivity {
override suspend fun execute(project: Project) {
project.getService(CodeXPOverlayController::class.java)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.github.ilovegamecoding.intellijcodexp.domain

import com.github.ilovegamecoding.intellijcodexp.enums.Event
import com.github.ilovegamecoding.intellijcodexp.models.CodeXPChallenge
import com.github.ilovegamecoding.intellijcodexp.models.CodeXPChallengeFactory
import com.github.ilovegamecoding.intellijcodexp.models.CodeXPLevel
import com.github.ilovegamecoding.intellijcodexp.models.CodeXPState

/**
* Handles CodeXP progress rules without depending on IntelliJ platform APIs or Swing.
*/
class CodeXPProgressEngine(
private val defaultChallenges: () -> List<CodeXPChallenge> = CodeXPChallengeFactory::createEventDefaultChallenges,
) {
fun initialize(state: CodeXPState) {
if (!state.hasExecuted) {
state.hasExecuted = true
}

Event.entries.forEach { event ->
if (!state.eventCounts.containsKey(event)) {
state.eventCounts[event] = 0
}
}

defaultChallenges().forEach { challenge ->
if (!state.challenges.containsKey(challenge.event)) {
state.challenges[challenge.event] = challenge
}
}
}

fun recordEvent(
state: CodeXPState,
event: Event,
): CodeXPProgressResult {
state.eventCounts[event] = state.eventCounts.getOrDefault(event, 0) + 1

val xpChanges = mutableListOf<CodeXPChange>()
xpChanges += increaseXP(state, event.xpValue)

val challenge = state.challenges[event] ?: return CodeXPProgressResult(event, xpChanges = xpChanges)
challenge.progress += 1

if (challenge.progress < challenge.goal) {
return CodeXPProgressResult(
event = event,
updatedChallenge = challenge,
xpChanges = xpChanges,
)
}

xpChanges += increaseXP(state, challenge.rewardXP)
val newChallenge = createNextChallenge(challenge)
state.completedChallenges.add(challenge)
state.challenges[event] = newChallenge

return CodeXPProgressResult(
event = event,
updatedChallenge = challenge,
completedChallenge = challenge,
newChallenge = newChallenge,
xpChanges = xpChanges,
)
}

private fun increaseXP(
state: CodeXPState,
incrementAmount: Long,
): CodeXPChange {
val beforeLevelInfo = CodeXPLevel.createLevelInfo(state.xp)
state.xp += incrementAmount
val currentLevelInfo = CodeXPLevel.createLevelInfo(state.xp)

return CodeXPChange(
beforeLevelInfo = beforeLevelInfo,
currentLevelInfo = currentLevelInfo,
)
}

private fun createNextChallenge(completedChallenge: CodeXPChallenge): CodeXPChallenge =
CodeXPChallenge(
event = completedChallenge.event,
name = completedChallenge.name,
description = completedChallenge.description,
rewardXP = completedChallenge.rewardXP + completedChallenge.rewardXPIncrement,
rewardXPIncrement = completedChallenge.rewardXPIncrement,
progress = 0,
goal = completedChallenge.goal + completedChallenge.goalIncrement,
goalIncrement = completedChallenge.goalIncrement,
)
}

data class CodeXPProgressResult(
val event: Event,
val updatedChallenge: CodeXPChallenge? = null,
val completedChallenge: CodeXPChallenge? = null,
val newChallenge: CodeXPChallenge? = null,
val xpChanges: List<CodeXPChange> = emptyList(),
)

data class CodeXPChange(
val beforeLevelInfo: CodeXPLevel,
val currentLevelInfo: CodeXPLevel,
) {
val isLevelUp: Boolean = beforeLevelInfo.level != currentLevelInfo.level && beforeLevelInfo.level != 0
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.github.ilovegamecoding.intellijcodexp.listeners
import com.github.ilovegamecoding.intellijcodexp.enums.Event
import com.github.ilovegamecoding.intellijcodexp.models.CodeXPChallenge
import com.github.ilovegamecoding.intellijcodexp.models.CodeXPLevel
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.util.messages.Topic

/**
Expand Down Expand Up @@ -30,7 +31,10 @@ interface CodeXPListener {
*
* @param levelInfo Level info.
*/
fun levelUp(levelInfo: CodeXPLevel)
fun levelUp(
levelInfo: CodeXPLevel,
dataContext: DataContext? = null,
)

/**
* Function that is called when challenge updated.
Expand All @@ -51,5 +55,6 @@ interface CodeXPListener {
fun challengeCompleted(
event: Event,
challenge: CodeXPChallenge,
dataContext: DataContext? = null,
)
}
Loading