Skip to content

provide support for 2024.3+#19

Merged
leewyatt merged 23 commits intoleewyatt:masterfrom
tuhin47:master
Apr 23, 2026
Merged

provide support for 2024.3+#19
leewyatt merged 23 commits intoleewyatt:masterfrom
tuhin47:master

Conversation

@tuhin47
Copy link
Copy Markdown
Contributor

@tuhin47 tuhin47 commented Dec 25, 2024

No description provided.

@leewyatt
Copy link
Copy Markdown
Owner

leewyatt commented Jan 4, 2025

@tuhin47 Thank you so much for updating the project to support the latest IDEA version! I really appreciate your contribution. I'll find some time to test it soon. 😊

leewyatt and others added 22 commits April 23, 2026 11:04
- Groovy build.gradle / settings.gradle -> Kotlin DSL (.kts)
- org.jetbrains.intellij 1.17.4 -> org.jetbrains.intellij.platform 2.11.0
- Gradle wrapper 8.8 -> 8.14 (plugin v2 requires 8.13+)
- Pin Gradle daemon to JBR 21.0.9 (system default JDK 25 is unparseable)
- commons-dbcp 1.4 -> commons-dbcp2 2.12.0 (BasicDataSource.setMaxActive -> setMaxTotal)
- commons-pool 1.6 dropped (dbcp2 bundles pool2)
- commons-dbutils 1.7 -> 1.8.1
- sqlite-jdbc 3.34.0 -> 3.46.1.0
- junit 5.6.0 -> 5.10.2
- since-build 222 -> 233 via patchPluginXml, until-build left empty
- pluginVersion 1.40 -> 1.41 via gradle.properties
- runIde: add -Dsun.java2d.metal=false for macOS UI-freeze mitigation

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every catch site now uses LOG.warn("<context>", e) with a context-
specific message describing the operation being attempted. Previously
exceptions went silently to stderr, making user bug reports hard to
diagnose.

LOG.warn (not LOG.error) — these are expected failure modes (IO, SQL,
JSON parse) where a "Report to JetBrains" dialog would be wrong.

Exceptions preserved:
- Comment-only printStackTrace references left alone
- ImportUtil.java:164 uses printStackTrace(PrintWriter) to format the
  trace into a user-facing import-error dialog; that's a legitimate
  stack-trace-to-string conversion, not a silent log drop

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
IntelliJ 2024.1+ new UI renders ToolWindow strip icons in a 20x20 slot.
Without this variant the platform upscales the 13x13 base SVG, which
looks fuzzy on the strip. Branded gradient colors preserved — they're
intentional (this isn't a monochrome system icon).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two user-facing assurances that the plugin's data is never trapped:

1. DatabaseBackupService
   - Runs in DatabaseBasicService constructor before initTable() / ALTER
   - Compares AppSettingsState.lastKnownPluginVersion to the live descriptor
     version; copies notebooks.db to ~/.ideaNotebooksFile/backups/ when they
     differ, then updates the setting
   - Keeps the last 5 backups, prunes older ones
   - Filename: notebooks_<timestamp>_from_<prev>_to_<cur>.db

2. Markdown-tree export (gear menu -> "Export as Markdown Tree")
   - Each note becomes its own .md with YAML frontmatter (title, type,
     source, offset_start/end, create/update times)
   - Structure: <dir>/<notebook>/<chapter>/<note>.md + <notebook>/_assets/
   - Image references rewritten to ../_assets/<filename>; imageDesc
     preserved as HTML comment
   - Filename sanitizer strips filesystem-reserved chars, caps length
   - Reuses CustomFileUtil.exportImagesToDirectory for image copying

AppSettingsState.lastKnownPluginVersion is a new field (default ""), not
a rename — existing users' persisted settings load correctly and trigger
the backup once on first launch of v1.41.

Bundle keys added in both EN and zh_CN.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Results of static + verifyPluginStructure/ProjectConfiguration pass:

Fixed
- Declared supportsKotlinPluginMode (K1 + K2) in kotlin-doc.xml, required
  by IDEA 2024.2.1+ when depending on org.jetbrains.kotlin
- Suppressed the matching DevKit warning on the <depends> line in the main
  plugin.xml — the validator can't follow config-file references

Intentionally left alone
- since-build=233 vs target platform 242: the gap is deliberate (supports
  2023.3+ IDE users). Matches JavaFX Tools reference config.
- sourceCompatibility=17 vs recommended 21: the platform suggests 21 but
  accepts 17-compiled bytecode on its 21 runtime. JavaFX Tools keeps 17.
- ChooseByNameContributor deprecated but still present in NoteFilterModel,
  OpenSearchBarAction, NoteChooseByname. Migration to ...ContributorEx
  requires untangling NoteChooseByname's inheritance from
  SearchRecordServiceImpl — that's L1 scope, not L0.

No Compatibility problems (removed APIs) found: no getBaseDir,
no old documentationProvider EP, no PathManager.getOptionsFile,
StartupActivity already migrated to ProjectActivity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- <version> bumped in plugin.xml (also injected by patchPluginXml from
  gradle.properties on build)
- <change-notes> rewritten for 1.41: auto-backup, Markdown tree export,
  build-chain modernization. HTML formatted, bilingual, old entries kept
- Actions ActivateNotebookToolWindow and OpenNotebookSearchBarAction
  no longer carry hardcoded text="..." attributes — resolved from bundle
  via action.<id>.text / action.<id>.description, matching the project
  convention used by EditorAddNoteAction and EditorInsertCodeAction
- Added matching bundle keys in EN and zh_CN
- Suppressed PluginXmlValidity on every <applicationService> /
  <projectService> with a comment explaining why @service migration is
  deferred (NoteChooseByname extends SearchRecordServiceImpl blocks the
  required `final`)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LICENSE is the canonical Apache 2.0 text fetched from apache.org.
Chose Apache 2.0 over MIT because it adds explicit patent grant +
patent retaliation (any contributor suing over patents loses their
license), plus more thorough warranty/liability disclaimers. Same
"AS IS" protection as MIT at the bottom line, with stronger defense
for the author against future contributor disputes.

README.md (English) and README.zh-CN.md (Chinese) cover the same
ground: what the plugin is, features, install, data locations,
shortcuts, export options, build instructions, project layout, and
the license/contact blurb. Cross-linked at the top of each file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ntime

Symptom: clicking through the Search dialog (or any first-time
BasicDataSource instantiation) crashes with

    LinkageError: loader constraint violation: when resolving method
    'org.slf4j.ILoggerFactory StaticLoggerBinder.getLoggerFactory()'
    ... different Class objects for the type org/slf4j/ILoggerFactory
    ... at BasicDataSource.<clinit>(BasicDataSource.java:71)

Root cause: dbcp2 2.10.0 (Jan 2024) bumped its transitive
commons-logging from 1.2 to 1.3.x. The new commons-logging includes
a Slf4jLogFactory that auto-bridges to SLF4J at runtime. Inside the
IntelliJ plugin classloader hierarchy, the platform's bundled SLF4J
and the one resolved through our plugin's dependency graph end up
as two different Class objects → LinkageError on first BasicDataSource
instantiation.

Fix: downgrade dbcp2 from 2.12.0 to 2.9.0 (the last release still
depending on commons-logging 1.2, which uses java.util.logging and
never touches SLF4J). Verified via `./gradlew dependencies`:

    commons-dbcp2:2.9.0
      +--- commons-pool2:2.10.0
      \--- commons-logging:1.2

No source changes required: setMaxTotal / setDriverClassName APIs
are identical between 2.9 and 2.12.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same class-of-bug as the dbcp2 downgrade, different library:

Startup crash:
    PluginException: Cannot init toolwindow NoteWindowFactory
    Caused by: LinkageError: loader constraint violation
      at org.slf4j.LoggerFactory.getILoggerFactory(...)
      at org.sqlite.JDBC.<clinit>(JDBC.java:26)       <-- here
      at o.a.c.dbcp2.DriverFactory.createDriver(...)

sqlite-jdbc 3.39.3.0 (Sept 2022) swapped java.util.logging for SLF4J.
Inside the IntelliJ plugin classloader hierarchy, the bundled slf4j-api
(pulled in through sqlite-jdbc) and the StaticLoggerBinder loaded by
another plugin's classloader end up with different org.slf4j.ILoggerFactory
Class objects -> LinkageError. Zero collision on 3.39.2.0 (last JUL
release) and earlier.

3.39.2.0 still gives us:
- Apple Silicon (aarch64) native binaries (added in 3.36)
- SQLite engine 3.39.2 (2022, modern enough)
- Security/stability fixes over the original 3.34.0

Verified via ./gradlew dependencies --configuration runtimeClasspath:
no slf4j-api anywhere on the runtime classpath. Plugin starts cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
NotebookDaoImpl.delete and ChapterDaoImpl.delete ran 2-3 separate
DELETE statements with no transaction. If any but the last statement
failed (disk full, DB lock, crash), the database was left with orphan
rows: chapters whose parent notebook was gone, or notes whose chapter
was gone. UI would show stale children that can't be re-selected.

Wrap both methods in setAutoCommit(false) + commit/rollback. On any
SQL failure, rollback and log; autoCommit is always restored in finally.

Implementation detail: bypass BaseDAO.update(conn, sql) in the
transactional path — that helper swallows SQLException internally
(existing project convention) which would defeat rollback. Use
queryRunner.update(conn, sql) directly so the catch block can actually
observe failures. The existing service-layer try/catch pattern is
preserved — delete() still never throws.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The two static fields exposed by NoteChooseByname were public mutable
ArrayLists shared across all projects and threads:

    public static List<NoteNavigationItem> list = new ArrayList<>();
    public static List<SearchRecord> records;

Every NoteChooseByname construction ran list.clear() then list.add()
in a loop with no synchronization. The IDE's Go To Symbol path invokes
getNames()/getItemsByName() from both EDT and background threads, and
opening a second project constructs another instance — concurrent
reads against an in-progress clear/rebuild could return partial or
inconsistent results, or ConcurrentModificationException.

Fix: store immutable snapshots in volatile fields. Constructor builds
a fresh List.copyOf(...) and assigns atomically — readers always see
either the old or new snapshot, never a partial state.

API surface change:
- Fields made private. Added public static getRecords() accessor.
- Two external readers updated to use the getter:
  - NoteItemPresentation.getLocationString
  - OpenSearchBarAction.actionPerformed
- Fixed a pre-existing latent NPE in both readers: .findFirst().get()
  would throw if the cache was empty (possible on fresh IDE start
  before any search). Now .orElse(null) + null-guard.

Default value changed from null to List.of() so pre-init access
returns empty list rather than NPEing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… (P1-1)

12 inline 'color:red' / 'color:blue' spans across three *TableCellEditor
files were hard-coded, so the conflict-warning balloons showed pure red
and pure blue regardless of IDE theme. Dark mode especially: the blue
was nearly invisible on the dark balloon background.

- PluginColors: added HTML_EMPHASIS_COLOR JBColor + two static helpers
  warnHtmlColor() and emphasisHtmlColor(). Each helper runs ColorUtil.toHex
  on the JBColor at call time, so the resolved hex tracks the current
  theme (switching light/dark picks up on the next notification).
- NotebookTableCellEditor, ChapterTableCellEditor, NoteTableCellEditor:
  swap the 12 inline color literals for the helper calls.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ImageRecord.imagePath is read from user-editable JSON during import and
from DB rows during export. The previous code trusted it:

    PluginConstant.IMAGE_DIRECTORY_PATH.resolve(imageRecord.getImagePath())

A JSON payload carrying "../../etc/passwd" (or any "../") would escape
the plugin's data directory. Could read/overwrite files outside
~/.ideaNotebooksFile/ during export, or during copy-to-export-dir
could escape the user-chosen export root.

Fix: new private resolveInside(Path base, String relative) that calls
.resolve().normalize() and verifies the result startsWith(base). Returns
null on rejection so callers skip the entry. LOG.warn records the
rejected path for visibility.

Call sites updated:
- exportImagesToDirectory: both source (IMAGE_DIRECTORY_PATH) and
  destination (export dir) are now resolved through resolveInside.
- deleteImagesAndThumb: protects against a rogue image name reaching
  the filesystem via the delete path.

Import-time directory copy (ImportUtil.java:45-48) is NOT changed —
it uses File.getName() which is already basename-only, and the parent
path is chosen by the user through the FileChooser, so not attacker-
controlled in the same way.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every time the user clicked an image row the ListSelectionListener ran
  ImageIcon icon = new ImageIcon(thumbFile.getAbsolutePath());
on the EDT. That constructor reads the file synchronously, and nothing
was cached — arrow-keying through a long list reloaded every thumbnail
from disk on each hop.

Added THUMB_CACHE: a synchronized access-ordered LinkedHashMap capped at
200 entries. Key is absolutePath + "?" + lastModified, so the cache
invalidates itself whenever a thumbnail on disk is replaced (no explicit
invalidation hook needed when the user updates or deletes an image).

Worst-case memory ≈ 200 × ~40KB (JPEG thumb) ≈ 8MB; typical working set
far smaller. Cache is static to survive panel re-creation across note
switches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Typing "java" into the note type field fired the DocumentListener four
times: 4 DB updates, 4 onNoteUpdated broadcasts to the message bus,
4 RecordListener handlers run across every open project. Multi-project
sessions were noticeably chatty.

Added an Alarm.ThreadToUse.SWING_THREAD scheduler disposed with the
project. Each keystroke cancels any pending request and schedules a
new one 500ms out. Only the last request runs — one DB write, one
broadcast per word typed.

The parallel focusLost listener is unchanged: on blur we still save
whatever the field currently holds (even if the debounced write didn't
fire yet, because 500ms hasn't elapsed). That path is idempotent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All three Task.Backgroundable ctors in ExportUtil passed `false` for
cancellable, so a slow export (large DB, many images) gave the user no
way out short of killing the IDE.

- exportJsonAndImage: false -> true, checkCanceled() before JSON writing
- exportMarkdownFile: false -> true, checkCanceled() before image copy
- exportMarkdownTree: false -> true, checkCanceled() at top of each
  notebook and chapter loop iteration

Export is side-effect-idempotent (partial outputs can be removed by the
user), so cancellation leaves no inconsistent state in the plugin's own
DB.

ImportUtil was already cancellable and is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ProjectActivity is a Kotlin suspend-fun interface; when implemented from
Java the suspend machinery reappears as the Continuation parameter. A
synchronous Java implementation must return Unit.INSTANCE to signal
"done", or COROUTINE_SUSPENDED for async work.

The previous code returned the continuation parameter itself, which is
neither of those — the platform's tolerant coroutine bridge accepted it
in practice (the plugin functioned), but it's formally wrong and could
break on any tightening of the coroutine protocol.

No behavior change expected on current platforms.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
P2-6 (ExportUtil.processToJsonString(int)): drop the Collections.singletonList
+ for-loop dance that iterated exactly once. Guard against findById returning
null (previously would have NPE'd on the implicit unboxing of getId()).

P2-7 (ImportUtil.addNotesFromJson): remove the redundant first loop that set
notebookId/chapterId on every note — the second loop sets the same fields
again while also populating the array. The first loop was a no-op.

P2-9 (DatabaseBasicService.isColumnExists): guard against queryRunner.query
returning null (rare, but possible on corrupted DB). Previously would have
NPE'd in the for-each, during first-run schema migration.

P2-10 (table cell editors): hoist setClickCountToStart(200) into
PluginConstant.TABLE_EDIT_CLICK_COUNT_START. Three identical magic numbers
became one named constant.

P2-12 (CustomUIUtil.writeImageToFile): delete the dead FileOutputStream
that was allocated but never used — ImageIO.write(destImage, ext, destFile)
opens its own stream internally. The now-unused java.io.FileOutputStream and
java.io.OutputStream imports are removed.

P2-13 (AppSettingsState.readOnlyMode): mark volatile. Import runs on a
background task and writes this field while action update() handlers read
it from BGT/EDT; volatile gives the needed happens-before.

P2-17 (CustomFileUtil.exportImagesToDirectory): count and LOG.warn missing
images so a "my export has broken links" bug report can be diagnosed from
idea.log without adding a user-facing balloon.

P2-18 (ImportUtil): surface an "empty file, nothing imported" notification
instead of silently returning — users were confusing empty-file imports
with successful ones. Bundle keys added in both en and zh_CN.

P2-20 (MainPanel): the three 0.5f splitter ratios are now one named
constant DEFAULT_SPLITTER_RATIO.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… desc font

Three long-standing GitHub issues that share the same area (ToolWindow
layout + description panel) fixed together:

GitHub leewyatt#8 — "default width" (2022-12)
  Resizing a column, closing the IDE, and reopening used to reset the
  column to its hard-coded default. Now persisted per-project.

  - ProjectStorage: three new float fields
    contentPaneProportion / leftPaneProportion / rightPaneProportion.
  - MainPanel: construct splitters with the default JBSplitter(false),
    then apply the saved values in the constructor. Register a
    PropertyChangeListener on "proportion" for each splitter — every
    drag writes back to ProjectStorage. No explicit "save" hook needed.
  - Listeners registered AFTER initial setProportion, so the programmatic
    startup calls don't echo-write themselves.

GitHub leewyatt#7 — "界面四块区域的布局比例" (2022-08)
  The original 0.5/0.5/0.5 equal split gave notebook/chapter/note/detail
  a 25/25/25/25 width share. Users kept manually widening the detail
  pane, but that width was lost on restart (see leewyatt#8 above). New defaults:

    contentPane = 0.3   (notebook+chapter : note+detail)
    leftPane    = 0.5   (notebook : chapter)
    rightPane   = 0.2   (note : detail)

  Roughly a 15/15/14/56 split — content gets majority of the width.
  Users can drag to taste; it will be saved.

GitHub leewyatt#6 — "font in description is a little bigger than other" (2022-04)
  fieldDesc used AppSettingsState.customFontName/Size — defaults
  MONOSPACED / 18pt. That made the description TextArea visibly larger
  than the labels / combos / type field around it.

  Smart default in DetailPanel.resolveDescFont: if the user's saved
  font values equal the original plugin defaults (MONOSPACED, 18), they
  almost certainly never actively picked them → use JBFont.regular()
  (IDE UI font). If they changed a value via Settings → Tools → Notebook,
  respect that choice. The existing onSetCustomFont message-bus listener
  still applies updates from the settings dialog.

Compatibility notes:
- ProjectStorage XML from v1.41 won't have the new proportion fields;
  XmlSerializer simply leaves the Java defaults (new content-biased
  ratios) in place on first load. No migration needed.
- Users who had customFontSize persisted at the plugin-default 18 will
  see a smaller description font after upgrade. This is the fix they
  asked for in leewyatt#6. Users who intentionally set a custom size see their
  choice preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The old "显示列表" row exposed four DumbAwareToggleAction icons — one
per panel. Each had its own isSelected() tied to its own panel's
visibility, so in the "full 4-column" mode three of them appeared
selected at the same time. Users interpreted them as independent
toggles (checkbox semantics) when they were actually radio-style mode
switches, and clicking any one rewrote visibility for all three panels.

Replaced with a single ComboBox of LayoutMode { FULL, CHAPTER_PLUS,
NOTE_PLUS, CONTENT_ONLY }. Each mode carries its own icon, bundle key,
and the intended visibility of notebook/chapter/note panels. The combo
renders icon + localized label (e.g. "Full (4 columns)" /
"完整 (4 列)"). Radio semantics now match the widget.

ProjectStorage changes:
- Added layoutMode field, default FULL. This is the source of truth
  going forward.
- Legacy notebookPaneVisible/chapterPaneVisible/notePaneVisible fields
  retained so old XML still deserializes.
- loadState migrates old XML: if layoutMode is absent but the legacy
  booleans are set to a non-trivial combination, LayoutMode is
  reconstructed via fromLegacyVisibility(...). All-false (plugin's
  original Java default) is treated as "no explicit preference" and
  upgrades to the new FULL default — new users and un-customized
  users now see the full 4-column layout on first open.
- getState syncs the legacy booleans back from layoutMode on every
  save, so downgrading to plugin 1.41 still produces correct
  visibility from XML.

DetailPanel changes:
- Added buildLayoutComboBox() + applyLayoutMode().
- Removed initNotebookVisibleAction / initChapterVisibleAction /
  initNoteVisibleAction / initDetailVisibleAction (230 lines of
  toggle-action boilerplate).
- Removed getVisiblePanelToolbar().
- computeWidth() no longer force-resets splitter proportions on
  every mode switch. That reset predated the per-drag persistence
  introduced for GitHub leewyatt#8 and was undoing the user's adjustments.
- controlViewVisible() no longer writes the three legacy booleans
  directly; ProjectStorage.getState() derives them from layoutMode.

MainPanel.resetPanesVisible() reads from layoutMode instead of the
three booleans.

Bundle: removed 4 mainPanel.action.showXxx.text keys (no longer
referenced from code); added mainPanel.layout.label and four
mainPanel.layout.* keys in both en and zh_CN.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two small but linked changes per plugin-author feedback:

1. Default LayoutMode changed from FULL to CONTENT_ONLY.
   On a fresh install the DB is empty, so three empty
   notebook/chapter/note lists are just clutter. The detail panel
   alone is the cleanest starting point; users open the Layout
   combo-box to reveal the hierarchy once they have data to browse.
   This also matches the pre-1.42 implicit behaviour (all visibility
   booleans defaulted to false), which the previous 1.42 default of
   FULL had silently changed.

2. loadState migration rewritten to not assume a specific default.
   Previously: "if layoutMode == FULL after copyBean, treat as
   un-migrated and rebuild from legacy booleans." That check was
   brittle — changing the Java default would silently break it.

   New: rebuild layoutMode from legacy booleans whenever the two
   disagree. post-1.42 saves keep them in sync via getState, so a
   disagreement only happens on upgrade from pre-1.42 XML (where the
   layoutMode field is absent and falls back to the current Java
   default). This approach is robust to any future default change.

Traced all eight combinations of (pre/post 1.42, four user-intent
modes) through the load path and each produces the expected final
layoutMode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@leewyatt leewyatt merged commit f9f1820 into leewyatt:master Apr 23, 2026
leewyatt added a commit that referenced this pull request Apr 23, 2026
…place

Description rewrite:
- Opening line highlights the plugin's actual value prop — code-aware,
  local-first, no cloud / account / telemetry — rather than "This is a
  note plugin." The 2021 description read like a feature manifest and
  missed the "why would I install this in 2026" question.
- Features regrouped by user scenario (Capture / Organize / Export &
  Backup / Configure) instead of by UI surface (dialog / button /
  menu item). Easier to scan on the Marketplace detail page.
- Removed the "Compact View / Full View" bullets — obsolete terms,
  replaced in v1.41 by the layout-mode ComboBox (Full / Chapter+ /
  Note+ / Detail only).
- Mentioned the v1.41 additions that users will actually care about:
  Markdown-tree export, auto-backup on upgrade, splitter-width
  persistence.
- @tuhin47 added to the thanks list.
- Bilingual structure preserved; en and zh_CN blocks separated by
  <hr/> for clarity.
- Updated CSS to style <h4> as thematic group headers.

Change-notes rewrite:
- Scoped to the v1.41 release only. Removed the <h3>1.40</h3> section
  (never published to Marketplace; users jumping from 1.38/1.39 → 1.41
  don't need notes for an intermediate they never saw).
- Restructured into five themes: Compatibility, Data safety, UX,
  Performance, Under the hood. Previous version was 5 mixed bullets.
- @tuhin47 credited for PR #19 (initial 2024.3 compat fix).
- Links to GitHub issues #6, #7, #8 where relevant so curious users
  can trace the change to the original report.
- Both en and zh_CN versions mirror the same structure.

XML validated via ElementTree.parse — no structural issues.
verifyPluginStructure reports only the known cosmetic
supportsKotlinPluginMode warning (validator limitation — declaration
is in kotlin-doc.xml which the verifier doesn't follow).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants