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
148 changes: 148 additions & 0 deletions .github/COMMENTING-GUIDELINES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Commenting Guidelines

Distilled from John Ousterhout's *A Philosophy of Software Design* (ch. 12–16).
One rule governs everything below:

> **Comments should describe things that aren't obvious from the code.**

A comment captures what was in the designer's mind but couldn't be expressed in
the code itself — the rationale, the constraints, the abstraction. If a comment
only restates the code, it has no value.

---

## What goes in a comment

There is real design information that code cannot express: the informal meaning
of a method, the units of a value, why a line exists, the rule the author
followed ("always call `a` before `b`"). Comments exist to record exactly this.

Comments also make abstraction possible. An abstraction hides complexity so you
can use a module without reading its implementation — and the only way to
describe an abstraction is in prose. Without comments, the sole abstraction of a
function is its signature, which leaves out too much to be useful (does
`substring(start, end)` include `end`? what if `start > end`?).

So a comment is good when it sits at a **different level of detail** than the
code: either lower (more precise) or higher (more intuitive). A comment at the
same level as the code is just restating it.

---

## Rules

**Don't repeat the code.** Before keeping a comment, ask: *could someone write
this just by looking at the code next to it?* If yes, delete it. A special case
of this: don't build the comment out of the words already in the name —
`// Normalize the resource name` above `getNormalizedResourceName()` adds nothing.

**Lower-level comments add precision.** Most valuable on declarations —
instance variables, parameters, return values, where the name and type aren't
enough. Spell out: units; whether bounds are inclusive or exclusive; what `null`
means if allowed; who owns/frees a resource; any invariant ("this list always
has at least one entry"). Describe what a variable *is*, not how the code
mutates it — think nouns, not verbs.

**Higher-level comments add intuition.** One sentence on what a block *does* and
why, omitting the mechanics. A reader who has that sentence can explain the rest
of the code themselves — and judge whether it's correct. These are harder to
write: ask yourself "what is the simplest thing I can say that explains
everything here?"

**Separate interface from implementation comments.** Interface comments tell a
caller what they need to use the thing — they *are* the abstraction.
Implementation comments explain how it works inside. Never let one leak into the
other. A caller should not have to read a method's body to call it correctly.

- *Class:* the abstraction it provides, what an instance represents, its
limitations. No method-by-method detail.
- *Method:* behavior from the caller's view; every parameter and the return
value, precisely; side effects; exceptions; preconditions. Keep
preconditions few, but document the ones that remain.
- The test for any fact: *does a caller need it to use this?* Wire formats,
internal data structures, transparent crash recovery — no, those are
implementation. A comparison being string-vs-integer, or whether requests
fire concurrently (affects performance) — yes.

**Implementation comments say what and why, not how.** Most short methods need
none. For longer ones, put a high-level line before each major block or
non-trivial loop. Always explain anything subtle the code can't show — a
bug-fix whose purpose isn't obvious gets a comment (reference the issue rather
than restating it: `// Fixes #436 — autolink overrun on pasted URLs`).

**Cross-module decisions need a findable home.** When a decision spans several
files, document it where developers will actually trip over it — e.g. at the enum
declaration they must edit, list every other place that needs updating. If
there's no natural center, keep a `designNotes` file and point to it from each
site (`// See "Zombies" in designNotes`).

---

## Write the comments first

Comments written last are bad comments: by then you've checked out mentally, your
memory of the design is fuzzy, and you write them by reading the code — so they
repeat it.

Instead, for a new class:

1. Write the class interface comment.
2. Write interface comments and signatures for the key public methods; leave the
bodies empty.
3. Iterate until the structure feels right.
4. Write declarations and comments for the key instance variables.
5. Fill in the bodies, adding implementation comments as needed.

When the code is done, the comments are done — there's no backlog. And it costs
almost nothing: typing code and comments together is a small fraction of total
development time.

**A comment is a complexity detector.** The comment for a method or variable
should be short *and* complete. If you can't write one that's both, the thing
you're describing is probably badly designed — that's the signal to fix the
design, not the comment.

> 🚩 If the interface comment has to describe the implementation, the method is
> too shallow. 🚩 If a comment merely repeats the code, it's noise. 🚩 If
> something is hard to describe, the design has a problem.

---

## Keep them alive

**Put the comment next to the code it describes.** The farther away, the less
likely it gets updated. A method's interface comment belongs right by its body,
not in a separate header. Push implementation comments down to the narrowest
scope they cover rather than stacking them at the top. As a corollary: the
farther a comment is from its code, the more abstract it should be.

**Comments belong in the code, not the commit log.** If a future developer will
need the information, put it where they'll see it. A commit message explaining a
subtle fix is invisible to the next person who "simplifies" it back into a bug.

**Don't duplicate.** Document each decision once, in the most obvious place, and
reference it from the others. Don't re-document one module inside another, and
don't restate things already in an external spec or manual — link to them.

**Check the diff before committing.** Scan every change and confirm the
surrounding comments still hold. This also catches stray debug code and stale
TODOs.

**Higher-level comments are easier to maintain** — they survive minor code
changes because they don't depend on details. Reserve precise, detailed comments
for the places that genuinely need them.

---

## Names are documentation too (ch. 14)

A good name reduces the need for comments. Make names **precise** — `getCount()`
counts *what*? — and make them **paint an image** of what the thing is and isn't,
in two or three words. A single vague name once cost the author a six-month bug
hunt: `block` meant both a disk block and a file block; `diskBlock` / `fileBlock`
would have prevented it. Don't settle for "close enough."

---

**The test for any comment:** is it something you *couldn't* read off the code,
and is it both short and complete?
5 changes: 5 additions & 0 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ We are really excited that you are interested in contributing to MarkText :tada:
- [Quick start](#quick-start)
- [Build instructions](#build-instructions)
- [Style guide](#style-guide)
- [Commenting guidelines](#commenting-guidelines)
- [Developer documentation](#developer-documentation)

## Philosophy
Expand Down Expand Up @@ -87,6 +88,10 @@ You can run ESLint (`pnpm run lint`) to help you to follow the style guide.
- no semicolons
- documentation: [JSDoc](https://github.com/jsdoc/jsdoc)

### Commenting Guidelines

When writing comments, please follow our [Commenting Guidelines](./COMMENTING-GUIDELINES.md). In short: a comment should describe what isn't obvious from the code — the rationale, units, invariants, and abstractions — rather than restating it. Reviewers check new and changed comments against these guidelines.

## Developer Documentation

Please [click here](https://marktext.me/docs/dev/overview) for more details.
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,10 @@ Enforced by ESLint + Prettier. Run `pnpm run lint` and `pnpm run typecheck` befo
- IPC channels are typed via the contract in `packages/desktop/src/shared/types/ipc.ts`
- The renderer is fully sandboxed — every IPC and Node access goes through `window.electron.*` / `window.fileUtils.*` etc. (typed in `packages/desktop/src/types/global.d.ts`)

### Comments

Follow `.github/COMMENTING-GUIDELINES.md` for every comment you write. The core rule: a comment must describe what isn't obvious from the code — rationale, units, invariants, ownership, the abstraction a caller needs — never restate the code or echo the words already in the name. Before finishing any change, review the comments you added or touched against that document, and delete any that only repeat the code. Prefer self-explanatory names over comments; when a comment is genuinely needed, keep it short and complete and place it next to the code it describes.

## Architecture: Three-Process Electron Model

All Electron processes live in `packages/desktop/`. Muya is a separate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2028,6 +2028,11 @@ onBeforeUnmount(() => {
top: 0;
left: 0;
overflow: hidden;
/* `z-index: -1` only hides the editor visually; `document.elementsFromPoint`
ignores stacking, so muya's mousemove-driven float tools (front button/menu,
table drag/column toolbars, preview toolbar) still re-trigger over the source
editor. Drop the subtree from hit-testing too so they cannot (#4731). */
pointer-events: none;
}

.editor-component {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
<template>
<div>
<div
v-if="showTitleBar"
class="title-bar-editor-bg"
:class="{ 'tabs-visible': showTabBar }"
/>
<div
v-if="showTitleBar"
class="title-bar"
:class="[
{ active: active },
Expand Down Expand Up @@ -142,6 +144,7 @@ import { storeToRefs } from 'pinia'
import { minimizePath, restorePath, maximizePath, closePath } from '../../assets/window-controls.js'
import { PATH_SEPARATOR } from '../../config'
import { isOsx as isOsxPlatform } from '@/util'
import { shouldShowInAppTitleBar } from './visibility'
import { useEditorStore } from '@/store/editor'
import { useI18n } from 'vue-i18n'
import { ArrowRight } from '@element-plus/icons-vue'
Expand Down Expand Up @@ -219,6 +222,10 @@ const showCustomTitleBar = computed(() => {
return titleBarStyle.value === 'custom' && !isOsx
})

const showTitleBar = computed(() => {
return shouldShowInAppTitleBar(titleBarStyle.value, isOsx)
})

watch(
() => props.filename,
(value) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function shouldShowInAppTitleBar(titleBarStyle: string, isOsx: boolean): boolean {
return titleBarStyle !== 'native' || isOsx
}
21 changes: 21 additions & 0 deletions packages/desktop/test/unit/specs/titlebar-visibility.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, it, expect } from 'vitest'
import { shouldShowInAppTitleBar } from '@/components/titleBar/visibility'

// #4210: with the native title bar (non-macOS, `frame: true`), the OS draws a
// title bar showing document.title AND the in-app custom title bar rendered the
// same filename — the title appeared twice. The in-app bar must be hidden when
// the native title bar is active. On macOS the window is always frameless, so
// the in-app bar must still show.
describe('shouldShowInAppTitleBar (#4210)', () => {
it('hides the in-app title bar for the native style on non-macOS', () => {
expect(shouldShowInAppTitleBar('native', false)).toBe(false)
})

it('still shows the in-app title bar on macOS even with the native style', () => {
expect(shouldShowInAppTitleBar('native', true)).toBe(true)
})

it('shows the in-app title bar for the custom style', () => {
expect(shouldShowInAppTitleBar('custom', false)).toBe(true)
})
})
47 changes: 47 additions & 0 deletions packages/muya/e2e/tests/editing/cross-block-enter.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { expect, test } from '../fixtures/muya';
import { getMarkdown } from '../helpers/api';
import { loadMarkdown } from '../helpers/keyboard';
import { editor } from '../helpers/selectors';

// #2443 — pressing Enter while a selection spans two blocks. The cross-block
// keydown handler only `preventDefault()`ed Backspace/Delete, so the browser's
// native Enter ran on top of the model edit and split/`<br>`-corrupted the
// contenteditable. Enter should behave like the same-block case: delete the
// selection and split at the caret into a new paragraph.

async function selectAcrossParagraphs(
page: import('@playwright/test').Page,
startOffset: number,
endOffset: number,
): Promise<void> {
await page.evaluate(({ startOffset, endOffset }) => {
const paras = document.querySelectorAll('.mu-paragraph');
const firstText = (el: Element): Text => {
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
return walker.nextNode() as Text;
};
const n1 = firstText(paras[0]);
const n2 = firstText(paras[1]);
const range = document.createRange();
range.setStart(n1, startOffset);
range.setEnd(n2, endOffset);
const sel = window.getSelection()!;
sel.removeAllRanges();
sel.addRange(range);
document.dispatchEvent(new Event('selectionchange'));
}, { startOffset, endOffset });
}

test.describe('cross-block selection + Enter (#2443)', () => {
test('replaces the selection with a paragraph break, not native DOM corruption', async ({ page }) => {
await loadMarkdown(page, 'Hello world\n\nFoo bar\n');
await page.locator(editor.paragraph).first().click();

// Select "world\n\nFoo": from after "Hello " (p1 offset 6) to after
// "Foo" (p2 offset 3).
await selectAcrossParagraphs(page, 6, 3);
await page.keyboard.press('Enter');

expect(await getMarkdown(page)).toBe('Hello \n\n bar\n');
});
});
37 changes: 37 additions & 0 deletions packages/muya/e2e/tests/inline/link-punctuation-wrap.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { expect, test } from '../fixtures/muya';

// #2258 / #3025 — punctuation right after a link wraps to its own line. With the
// caret outside a link its `](url)` markers render as zero-size `.mu-hide
// .mu-remove` spans. As `inline-block` (atomic) boxes they introduce soft-wrap
// opportunities between the link and the following text, so a trailing period
// can break onto its own line. This drives the REAL stylesheet rule: a hidden
// marker sandwiched between a word and a period, in a container just wide enough
// for the word, must not push the period to a new line.

test.describe('punctuation after a link does not wrap (#2258/#3025)', () => {
test('a zero-size hidden marker introduces no wrap opportunity before a period', async ({ page }) => {
await page.goto('http://localhost:5174/');

const sameLine = await page.evaluate(() => {
const host = document.createElement('div');
host.style.cssText = 'position:absolute;top:0;left:0;font-size:16px;line-height:1.5;white-space:normal;';
host.innerHTML
= '<span id="w">aaaa</span>'
+ '<span class="mu-hide mu-remove">](http://example.com)</span>'
+ '<span id="d">.</span>';
document.body.appendChild(host);

const w = document.getElementById('w')!;
// Just wide enough for the word — the period is the only overflow
// candidate, so it wraps iff a break opportunity exists before it.
host.style.width = `${Math.ceil(w.getBoundingClientRect().width) + 2}px`;

const wTop = Math.round(w.getBoundingClientRect().top);
const dTop = Math.round(document.getElementById('d')!.getBoundingClientRect().top);
host.remove();
return dTop <= wTop;
});

expect(sameLine).toBe(true);
});
});
Loading
Loading