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
3 changes: 3 additions & 0 deletions .sonarcloud.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
sonar.sources=je_editor
sonar.tests=test
sonar.python.version=3.10, 3.11, 3.12
5 changes: 4 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ All code must pass SonarQube and Codacy quality gates. Adhere to the following r
## Git & Commit Rules

- **Commit messages**: Write in English, concise, imperative mood (e.g., "Add plugin hot-reload support")
- **No AI attribution**: Do not mention any AI tool, assistant, or model name in commit messages or code comments
- **No AI attribution (mandatory)**: Never mention any AI tool, assistant, agent, model name, or vendor in commit messages, commit trailers, branch names, PR titles, PR descriptions, issue text, code comments, or documentation
- No `Co-Authored-By:` trailers referencing an AI tool or model
- No "Generated with ...", "Created by ...", or similar footers in commits or PR bodies
- PR titles and bodies describe **what changed and why** — nothing about how the change was authored
- **Branch strategy**: `main` = stable release, `dev` = active development
- **Clean commits**: Each commit should be a single logical change; no unrelated changes bundled together
70 changes: 68 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ JEDITOR achieves up to **1000% faster performance** compared to the original JEd

| Category | Features |
|---|---|
| **Editor** | Multi-tab editing, syntax highlighting, auto-completion (Jedi), line numbers, current line highlighting, search & replace (regex) |
| **Editor** | Multi-tab editing, syntax highlighting, auto-completion (Jedi), line numbers, current line highlighting, search & replace (regex), code folding, bookmarks, occurrence highlighting, line operations |
| **Navigation** | Command palette, quick open (go to file), go to symbol, document outline, navigation history (back/forward), TODO/FIXME task panel |
| **Execution** | Run Python scripts (F5), debug mode (F9), shell commands, virtual environment detection |
| **Code Quality** | YAPF formatting, PEP8 checking, Ruff linting, JSON reformatting |
| **Git** | Branch management, commit history, side-by-side diff viewer, staging, audit logging |
Expand All @@ -85,7 +86,7 @@ JEDITOR achieves up to **1000% faster performance** compared to the original JEd
| **Plugins** | Custom syntax highlighting, UI translations, run configurations, auto-discovery |
| **UI** | Dark/light themes (Qt Material), font customization, dockable panels, system tray, toolbar |
| **i18n** | English, Traditional Chinese, extensible via plugins |
| **Files** | Auto-save, multi-encoding support (UTF-8, GBK, Latin-1, etc.), recent files |
| **Files** | Auto-save, multi-encoding support (UTF-8, GBK, Latin-1, etc.), recent files, multi-file session restore |

---

Expand Down Expand Up @@ -173,8 +174,26 @@ The editor launches in a maximized window with a dark amber theme by default.
- **Auto-completion** -- Context-aware code suggestions powered by Jedi.
- **Line numbers** -- Displayed alongside the editor with current line highlighting.
- **Search & Replace** -- Search within the current file, across folders, or project-wide with regex and case-sensitive options. Runs in background threads for large projects.
- **Code Folding** -- Collapse and expand indented blocks (functions, classes, loops) from the gutter fold triangles or the keyboard. Folding is indentation-based and only toggles line visibility -- it never modifies the text, so saving always writes the complete file. Folds self-heal after edits: a fold whose header no longer exists simply reopens instead of hiding the wrong lines.
- **Bookmarks** -- Mark lines and jump between them with the keyboard, or click the gutter to toggle. Bookmarks are anchored to the text (via `QTextCursor`), so they follow their code when lines are inserted or removed above them instead of drifting.
- **Occurrence Highlighting** -- Placing the caret on an identifier highlights every other whole-word occurrence of it in the file. Keywords and single characters are ignored, and the scan is skipped on very large files to keep caret movement instant.
- **Line Operations** -- Delete the current line or selection (`Ctrl+Shift+D`), sort selected lines (`Ctrl+Alt+S`), join selected lines into one (`Ctrl+Shift+J`), and (from the Text menu) natural sort, remove duplicate lines, remove blank lines, reverse line order, or align lines on a delimiter (e.g. `=`). Each is a single undo step.
- **Duplicate** (`Ctrl+D`) -- Duplicates the selection when there is one (selecting the new copy), or the whole line when there isn't.
- **Case Conversion** (Text menu) -- Uppercase or lowercase the current selection, keeping it selected.
- **Smart Selection** -- Expand the selection outward through word → line → enclosing indented blocks → whole file (`Ctrl+Alt+Right`), and shrink it back (`Ctrl+Alt+Left`). Shrinking only retraces expansions, and a manual selection change resets the history.
- **Increment / Decrement Number** -- Bump the integer under the caret up or down (`Ctrl+Alt+Up` / `Ctrl+Alt+Down`), handling negative signs and growing widths.
- **Rename in File** (`F2`) -- Rename every whole-word occurrence of the identifier under the caret across the file as a single undo step. Word boundaries protect partial matches (renaming `val` never touches `value`).
- **Navigation History** -- Jump back and forward through your cursor-jump history like a browser (`Alt+Left` / `Alt+Right`). A jump records both where you came from and where you went, so "back" returns to where you started.
- **Document Outline** -- A dockable tree of the current file's classes, methods, functions and module variables (parsed with `ast`, so no code runs). Double-click to jump to a definition.
- **Variable Inspector** -- Inspect and debug variables during code execution.

### Navigation

- **Command Palette** (Ctrl+Shift+A) -- Fuzzy-search every menu command by name or menu path and run it without hunting through menus. Matches are ranked by word boundaries, consecutive characters and prefixes, and each row shows the command's own keyboard shortcut.
- **Quick Open / Go to File** (Ctrl+P) -- Fuzzy-search the project tree by file name *or* folder path. Indexing runs in a background thread, skipping VCS, cache, virtualenv and build directories along with binary file types. Typing `>` at the start switches the same picker into command mode.
- **Go to Symbol** (Ctrl+Shift+O) -- Jump to any class, function, method or module-level variable in the current Python file. Symbols are parsed with the standard library `ast` module, so no user code is ever executed, and a file that does not parse simply yields no symbols instead of erroring while you type.
- **TODO Panel** (Tab > Tools, or as a dock) -- Scans the project for `TODO`, `FIXME`, `HACK`, `XXX`, `BUG`, `NOTE` and `OPTIMIZE` comments across Python, C-like, HTML, SQL and other comment styles. Filter by tag and double-click a row to open that file at that line. Tags are only reported when they follow a comment marker, so ordinary strings are not misreported.

### Code Execution & Debugging

- **Run Python scripts** (F5) -- Execute the current file with real-time output streaming.
Expand All @@ -190,12 +209,18 @@ The editor launches in a maximized window with a dark amber theme by default.
- **PEP8 checking** (Ctrl+Alt+P) -- Validate code against PEP8 style guidelines.
- **Ruff linting** -- Fast, comprehensive Python linting in a background thread.
- **JSON reformatting** (Ctrl+J) -- Pretty-print and validate JSON content.
- **Trim Trailing Whitespace** (Text menu) -- Strip trailing whitespace from every line as one undo step, preserving the caret position.
- **Convert Indentation** (Text menu) -- Convert leading indentation between tabs and spaces (using your indent size). Only leading whitespace is touched, so tabs and spaces inside strings are never altered.
- **Configurable indent width** -- Tab-indent, unindent and Enter auto-indent all honour the configured indent size (`Text > Indent Size`), and the indent width is auto-detected from a file's own content when it is opened.
- **Text transforms** (Text menu) -- Case conversion (upper / lower / swap / title), naming-style conversion (`snake_case` / `camelCase` / `PascalCase` / `kebab-case`), number-base conversion (hex / decimal / binary), and encode/decode helpers (Base64, URL, HTML entities, JSON string escaping). Decoders that fail leave the text untouched.
- **Statistics** (Text menu) -- Line, word and character counts for the whole document or the current selection.

### File Operations

- **Create, open, save** files with standard shortcuts (Ctrl+N, Ctrl+O, Ctrl+S).
- **Open folders** (Ctrl+K) -- Navigate project directory structures.
- **Auto-save** -- Automatic periodic file saving to prevent data loss.
- **Session restore** -- Reopens every file that was open at the last shutdown, not just the last one. Missing, duplicate and already-open files are skipped, the list is capped, and a corrupt or hand-edited settings file can never block startup. Disable by setting `restore_session` to `false` in `.jeditor/user_setting.json`.
- **Multi-encoding** -- Seamlessly handle UTF-8, GBK, Latin-1, and other encodings with automatic detection.
- **Recent files** -- Quick access to previously opened files.

Expand Down Expand Up @@ -270,6 +295,25 @@ Plugins are automatically discovered from the `jeditor_plugins/` directory. See
| `Ctrl+O` | Open file |
| `Ctrl+K` | Open folder |
| `Ctrl+S` | Save file |
| `Ctrl+Shift+A` | Command palette |
| `Ctrl+P` | Quick open (go to file) |
| `Ctrl+Shift+O` | Go to symbol |
| `Ctrl+Shift+[` | Toggle fold at cursor |
| `Ctrl+Alt+[` | Fold all |
| `Ctrl+Alt+]` | Unfold all |
| `Ctrl+Alt+K` | Toggle bookmark |
| `Ctrl+Alt+L` | Next bookmark |
| `Ctrl+Alt+J` | Previous bookmark |
| `Alt+Left` | Navigate back |
| `Alt+Right` | Navigate forward |
| `Ctrl+Shift+D` | Delete current line / selection |
| `Ctrl+Alt+S` | Sort selected lines |
| `Ctrl+Shift+J` | Join selected lines |
| `Ctrl+Alt+Right` | Expand selection |
| `Ctrl+Alt+Left` | Shrink selection |
| `Ctrl+Alt+Up` | Increment number under caret |
| `Ctrl+Alt+Down` | Decrement number under caret |
| `F2` | Rename occurrences in file |
| `Ctrl+J` | Reformat JSON |
| `Ctrl+Shift+Y` | YAPF Python format |
| `Ctrl+Alt+P` | PEP8 format checker |
Expand All @@ -289,8 +333,10 @@ je_editor/
│ ├── browser/ # Embedded web browser
│ ├── code/ # Core code editing
│ │ ├── auto_save/ # Automatic file saving
│ │ ├── bookmark/ # Bookmark manager (QTextCursor-anchored)
│ │ ├── code_format/ # YAPF & PEP8 formatting
│ │ ├── code_process/ # Program execution (ExecManager)
│ │ ├── folding/ # Code folding manager
│ │ ├── shell_process/ # Shell execution (ShellManager)
│ │ ├── syntax/ # Syntax highlighting engine
│ │ ├── plaintext_code_edit/ # Plain text editor widget
Expand All @@ -305,14 +351,17 @@ je_editor/
│ │ └── git_client/ # Branch & commit UI
│ └── main_ui/ # Main editor window
│ ├── ai_widget/ # AI chat panel
│ ├── command_palette/ # Command palette, quick open, go to symbol
│ ├── console_widget/ # Interactive console
│ ├── dock/ # Dockable widget management
│ ├── editor/ # Tab-based editor
│ ├── ipython_widget/ # Jupyter/IPython console
│ ├── menu/ # Menu bar system
│ ├── outline_panel/ # Document outline (symbol tree)
│ ├── plugin_browser/ # Plugin management UI
│ ├── save_settings/ # Settings persistence
│ ├── system_tray/ # System tray integration
│ ├── todo_panel/ # TODO/FIXME task panel
│ └── toolbar/ # Toolbar actions
├── code_scan/ # Code scanning
│ ├── ruff_thread.py # Ruff linter (threaded)
Expand All @@ -325,13 +374,30 @@ je_editor/
├── plugins/ # Plugin system
│ └── plugin_loader.py # Dynamic plugin loading
├── utils/ # Utilities
│ ├── align/ # Align lines on a delimiter (Qt-free)
│ ├── bookmark/ # Bookmark navigation logic (Qt-free)
│ ├── case_convert/ # Naming-style conversion (Qt-free)
│ ├── code_folding/ # Fold region computation (Qt-free)
│ ├── command_palette/ # Fuzzy matching & ranking (Qt-free)
│ ├── encode_decode/ # Base64/URL/HTML/JSON transforms (Qt-free)
│ ├── encodings/ # Encoding detection
│ ├── exception/ # Custom exceptions
│ ├── file/ # File I/O (open/save)
│ ├── file_scan/ # Shared ignore rules, file indexer, TODO scanner
│ ├── indentation/ # Tab/space conversion + indent detection (Qt-free)
│ ├── json_format/ # JSON formatting
│ ├── line_ops/ # Line operation transforms (Qt-free)
│ ├── logging/ # Logging setup
│ ├── multi_language/ # i18n (English, Traditional Chinese)
│ ├── navigation/ # Cursor jump history (Qt-free)
│ ├── number_ops/ # Number-under-caret adjustment (Qt-free)
│ ├── occurrence/ # Word occurrence finding + whole-word rename (Qt-free)
│ ├── redirect_manager/ # Output stream redirection
│ ├── selection/ # Smart selection ranges (Qt-free)
│ ├── session/ # Multi-file session restore (Qt-free)
│ ├── symbols/ # Python symbol extraction + outline tree (ast-based)
│ ├── text_cleanup/ # Trailing whitespace / newline cleanup (Qt-free)
│ ├── text_stats/ # Line/word/char statistics (Qt-free)
│ └── venv_check/ # Virtual environment detection
├── __init__.py # Public API
├── __main__.py # CLI entry point
Expand Down
Empty file.
134 changes: 134 additions & 0 deletions je_editor/pyside_ui/code/bookmark/bookmark_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
"""
書籤管理器(Qt 整合層)
Bookmark manager (Qt integration layer).

每個書籤以 ``QTextCursor`` 錨定在該行開頭。當上方插入或刪除文字時,
``QTextCursor`` 會自動跟著移動,因此書籤能正確地跟著程式碼移動而不會漂移。
Each bookmark is anchored by a ``QTextCursor`` at the line start. A ``QTextCursor``
moves automatically when text is inserted or removed above it, so bookmarks follow
the code they mark instead of drifting.
"""
from __future__ import annotations

from typing import TYPE_CHECKING

from PySide6.QtGui import QTextCursor

from je_editor.utils.bookmark.bookmark_navigation import next_bookmark, prev_bookmark

if TYPE_CHECKING:
from je_editor.pyside_ui.code.plaintext_code_edit.code_edit_plaintext import CodeEditor


class BookmarkManager:
"""
管理單一編輯器的書籤
Manage the bookmarks of one editor.
"""

def __init__(self, editor: CodeEditor) -> None:
"""
:param editor: 被管理的程式碼編輯器 / The code editor being managed
"""
self._editor = editor
self._cursors: list[QTextCursor] = []

def bookmarked_lines(self) -> list[int]:
"""
取得目前所有書籤的行號
Return the current bookmark line numbers.

每次即時查詢游標的行號,因此反映文字編輯後的最新位置。
The line is read from each cursor live, so it reflects edits made since.

:return: 由小到大排序、無重複的行號(0 起算)/ Sorted, unique 0-based line numbers
"""
return sorted({cursor.blockNumber() for cursor in self._cursors})

def is_bookmarked(self, line: int) -> bool:
"""
判斷某一行是否有書籤
Return whether a line is bookmarked.

:param line: 行號(0 起算)/ The line (0-based)
:return: 有書籤時為 ``True`` / ``True`` when bookmarked
"""
return any(cursor.blockNumber() == line for cursor in self._cursors)

def toggle(self, line: int) -> bool:
"""
切換某一行的書籤
Toggle the bookmark on a line.

:param line: 行號(0 起算)/ The line (0-based)
:return: 切換後該行有書籤時為 ``True`` / ``True`` when the line ends up bookmarked
"""
existing = [cursor for cursor in self._cursors if cursor.blockNumber() == line]
if existing:
self._cursors = [cursor for cursor in self._cursors if cursor.blockNumber() != line]
return False
cursor = self._cursor_at_line(line)
if cursor is None:
return False
self._cursors.append(cursor)
return True

def toggle_current(self) -> bool:
"""
切換游標所在行的書籤
Toggle the bookmark on the line holding the caret.

:return: 切換後該行有書籤時為 ``True`` / ``True`` when the line ends up bookmarked
"""
return self.toggle(self._editor.textCursor().blockNumber())

def clear(self) -> None:
"""清除所有書籤 / Remove every bookmark."""
self._cursors.clear()

def go_to_next(self, wrap: bool = True) -> int | None:
"""
跳到下一個書籤
Jump to the next bookmark.

:param wrap: 找不到時是否從頭繞回 / Whether to wrap to the first bookmark
:return: 跳到的行號,沒有書籤時回傳 ``None`` / The line jumped to, or ``None``
"""
target = next_bookmark(
self.bookmarked_lines(), self._editor.textCursor().blockNumber(), wrap)
return self._jump_to(target)

def go_to_previous(self, wrap: bool = True) -> int | None:
"""
跳到上一個書籤
Jump to the previous bookmark.

:param wrap: 找不到時是否從尾端繞回 / Whether to wrap to the last bookmark
:return: 跳到的行號,沒有書籤時回傳 ``None`` / The line jumped to, or ``None``
"""
target = prev_bookmark(
self.bookmarked_lines(), self._editor.textCursor().blockNumber(), wrap)
return self._jump_to(target)

def _cursor_at_line(self, line: int):
"""建立錨定在指定行開頭的游標 / Build a cursor anchored at a line's start."""
block = self._editor.document().findBlockByNumber(line)
if not block.isValid():
return None
cursor = QTextCursor(block)
cursor.movePosition(QTextCursor.MoveOperation.StartOfBlock)
return cursor

def _jump_to(self, line: int | None) -> int | None:
"""把編輯器游標移到指定行 / Move the editor caret to a line."""
if line is None:
return None
block = self._editor.document().findBlockByNumber(line)
if not block.isValid():
return None
cursor = self._editor.textCursor()
cursor.setPosition(block.position())
self._editor.setTextCursor(cursor)
self._editor.centerCursor()
self._editor.highlight_current_line()
return line
Empty file.
Loading
Loading