Skip to content

Commit c479364

Browse files
authored
feat(editor): add configurable word wrap indentation (#2886)
* feat(editor): add configurable word wrap indentation * chore: translation strings * fix
1 parent 77b0b54 commit c479364

38 files changed

Lines changed: 286 additions & 41 deletions

‎src/cm/indentedLineWrapping.ts‎

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
EditorState,
33
type Extension,
4+
Facet,
45
RangeSetBuilder,
56
StateEffect,
67
} from "@codemirror/state";
@@ -13,15 +14,26 @@ import {
1314
} from "@codemirror/view";
1415

1516
const wrapWidth = StateEffect.define<number>();
17+
export type WrappingIndent = "none" | "same" | "indent" | "deepIndent";
18+
const wrappingIndent = Facet.define<WrappingIndent, WrappingIndent>({
19+
combine: (values) => values[0] ?? "same",
20+
});
1621

1722
/** Keep tab stops on the first visual row unchanged by the negative indent. */
1823
export function wrappedIndentColumns(
1924
text: string,
2025
tabSize: number,
2126
limit: number,
27+
mode: WrappingIndent = "same",
2228
): number {
23-
// Unindented/minified lines need no tab scan, even when they are megabytes long.
24-
if (limit <= 0 || (text[0] !== " " && text[0] !== "\t")) return 0;
29+
const extra =
30+
mode === "indent" ? tabSize : mode === "deepIndent" ? 2 * tabSize : 0;
31+
if (mode === "none" || limit <= 0) return 0;
32+
if (text[0] !== " " && text[0] !== "\t") {
33+
// Extra levels are whole tab stops, so content tabs cannot affect them.
34+
// Keep the width cap tab-aligned too, avoiding a scan even in narrow panes.
35+
return Math.min(extra, Math.floor(limit / tabSize) * tabSize);
36+
}
2537
// Include content tabs deliberately: negative text-indent changes the origin
2638
// of every tab stop on the first visual row. For " key\tvalue", using 2ch
2739
// shifts "value" left by two columns in Chromium; 4ch preserves its position.
@@ -36,7 +48,7 @@ export function wrappedIndentColumns(
3648
else break;
3749
if (columns >= cap) return cap;
3850
}
39-
return Math.min(cap, Math.ceil(columns / step) * step);
51+
return Math.min(cap, Math.ceil((columns + extra) / step) * step);
4052
}
4153

4254
interface CachedIndent {
@@ -51,6 +63,7 @@ function decorate(
5163
): { decorations: DecorationSet; cache: Map<number, CachedIndent> } {
5264
const builder = new RangeSetBuilder<Decoration>();
5365
const tabSize = view.state.facet(EditorState.tabSize);
66+
const mode = view.state.facet(wrappingIndent);
5467
const cache = new Map<number, CachedIndent>();
5568
const styles = new Map<number, Decoration>();
5669
let lastLine = -1;
@@ -60,7 +73,7 @@ function decorate(
6073
if (line.from > lastLine) {
6174
let entry = previous.get(line.from);
6275
if (!entry || entry.text !== line.text) {
63-
const columns = wrappedIndentColumns(line.text, tabSize, limit);
76+
const columns = wrappedIndentColumns(line.text, tabSize, limit, mode);
6477
let decoration = columns ? styles.get(columns) : null;
6578
if (columns && !decoration) {
6679
decoration = Decoration.line({
@@ -133,7 +146,10 @@ const plugin = ViewPlugin.fromClass(
133146
const tabSizeChanged =
134147
update.startState.facet(EditorState.tabSize) !==
135148
update.state.facet(EditorState.tabSize);
136-
if (changed || tabSizeChanged) {
149+
const modeChanged =
150+
update.startState.facet(wrappingIndent) !==
151+
update.state.facet(wrappingIndent);
152+
if (changed || tabSizeChanged || modeChanged) {
137153
this.cache.clear();
138154
} else if (update.docChanged) {
139155
// Keep unchanged visible lines cached when earlier edits shift them.
@@ -148,7 +164,8 @@ const plugin = ViewPlugin.fromClass(
148164
changed ||
149165
update.docChanged ||
150166
update.viewportChanged ||
151-
tabSizeChanged
167+
tabSizeChanged ||
168+
modeChanged
152169
) {
153170
const result = decorate(update.view, this.limit, this.cache);
154171
this.decorations = result.decorations;
@@ -168,9 +185,13 @@ const plugin = ViewPlugin.fromClass(
168185
* tabs occur after the leading whitespace. Oversized indents are capped
169186
* at half the available columns so narrow panes still have room for content.
170187
*/
171-
export function indentedLineWrapping(): Extension {
188+
export function indentedLineWrapping(mode: WrappingIndent = "same"): Extension {
189+
if (mode === "none") return EditorView.lineWrapping;
190+
// Settings imported from older or manually edited files may be invalid.
191+
if (mode !== "indent" && mode !== "deepIndent") mode = "same";
172192
return [
173193
EditorView.lineWrapping,
194+
wrappingIndent.of(mode),
174195
plugin,
175196
EditorView.baseTheme({
176197
".cm-line.cm-wrap-indent": {

‎src/lang/ar-ye.json‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -876,5 +876,10 @@
876876
"plugin-review:error-max-length": "يجب ألا يتجاوز التقييم {max} حرفًا.",
877877
"plugin-review:error-downvote-comment": "أخبر المطوّر بما يمكن تحسينه.",
878878
"plugin-review:error-empty": "أضف تعليقًا أو اختر «أعجبني».",
879-
"plugin-review:unrated": "غير مقيّم"
879+
"plugin-review:unrated": "غير مقيّم",
880+
"wrapped line indent": "Word wrap indentation",
881+
"wrap-indent-same": "Same",
882+
"wrap-indent-indent": "Indent (+1 level)",
883+
"wrap-indent-deep": "Deep indent (+2 levels)",
884+
"settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size."
880885
}

‎src/lang/be-by.json‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -876,5 +876,10 @@
876876
"plugin-review:error-max-length": "Водгук павінен змяшчаць не больш за {max} сімвалаў.",
877877
"plugin-review:error-downvote-comment": "Раскажыце распрацоўшчыку, што можна палепшыць.",
878878
"plugin-review:error-empty": "Дадайце каментарый або выберыце «Падабаецца».",
879-
"plugin-review:unrated": "Без ацэнкі"
879+
"plugin-review:unrated": "Без ацэнкі",
880+
"wrapped line indent": "Word wrap indentation",
881+
"wrap-indent-same": "Same",
882+
"wrap-indent-indent": "Indent (+1 level)",
883+
"wrap-indent-deep": "Deep indent (+2 levels)",
884+
"settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size."
880885
}

‎src/lang/bn-bd.json‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -876,5 +876,10 @@
876876
"plugin-review:error-max-length": "পর্যালোচনা {max} অক্ষর বা তার কম হতে হবে।",
877877
"plugin-review:error-downvote-comment": "কী উন্নত করা যায় তা ডেভেলপারকে জানান।",
878878
"plugin-review:error-empty": "একটি মন্তব্য যোগ করুন বা পছন্দ নির্বাচন করুন।",
879-
"plugin-review:unrated": "এখনও মূল্যায়ন নেই"
879+
"plugin-review:unrated": "এখনও মূল্যায়ন নেই",
880+
"wrapped line indent": "Word wrap indentation",
881+
"wrap-indent-same": "Same",
882+
"wrap-indent-indent": "Indent (+1 level)",
883+
"wrap-indent-deep": "Deep indent (+2 levels)",
884+
"settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size."
880885
}

‎src/lang/cs-cz.json‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -876,5 +876,10 @@
876876
"plugin-review:error-max-length": "Recenze může mít nejvýše {max} znaků.",
877877
"plugin-review:error-downvote-comment": "Sdělte vývojáři, co by se dalo zlepšit.",
878878
"plugin-review:error-empty": "Přidejte komentář nebo zvolte Líbí se mi.",
879-
"plugin-review:unrated": "Nehodnoceno"
879+
"plugin-review:unrated": "Nehodnoceno",
880+
"wrapped line indent": "Word wrap indentation",
881+
"wrap-indent-same": "Same",
882+
"wrap-indent-indent": "Indent (+1 level)",
883+
"wrap-indent-deep": "Deep indent (+2 levels)",
884+
"settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size."
880885
}

‎src/lang/de-de.json‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -876,5 +876,10 @@
876876
"plugin-review:error-max-length": "Die Bewertung darf höchstens {max} Zeichen lang sein.",
877877
"plugin-review:error-downvote-comment": "Sage dem Entwickler, was verbessert werden könnte.",
878878
"plugin-review:error-empty": "Füge einen Kommentar hinzu oder wähle „Gefällt mir“.",
879-
"plugin-review:unrated": "Nicht bewertet"
879+
"plugin-review:unrated": "Nicht bewertet",
880+
"wrapped line indent": "Word wrap indentation",
881+
"wrap-indent-same": "Same",
882+
"wrap-indent-indent": "Indent (+1 level)",
883+
"wrap-indent-deep": "Deep indent (+2 levels)",
884+
"settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size."
880885
}

‎src/lang/en-us.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@
9292
"success": "Success",
9393
"tab size": "Tab size",
9494
"text wrap": "Text wrap / Word wrap",
95+
"wrapped line indent": "Word wrap indentation",
96+
"wrap-indent-same": "Same",
97+
"wrap-indent-indent": "Indent (+1 level)",
98+
"wrap-indent-deep": "Deep indent (+2 levels)",
99+
"settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size.",
95100
"theme": "Theme",
96101
"title-listfiles": "List files",
97102
"ui zoom": "UI zoom",

‎src/lang/es-sv.json‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -876,5 +876,10 @@
876876
"plugin-review:error-max-length": "La reseña debe tener {max} caracteres o menos.",
877877
"plugin-review:error-downvote-comment": "Dile al desarrollador qué podría mejorar.",
878878
"plugin-review:error-empty": "Agrega un comentario o elige Me gusta.",
879-
"plugin-review:unrated": "Sin calificar"
879+
"plugin-review:unrated": "Sin calificar",
880+
"wrapped line indent": "Word wrap indentation",
881+
"wrap-indent-same": "Same",
882+
"wrap-indent-indent": "Indent (+1 level)",
883+
"wrap-indent-deep": "Deep indent (+2 levels)",
884+
"settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size."
880885
}

‎src/lang/fr-fr.json‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -876,5 +876,10 @@
876876
"plugin-review:error-max-length": "L’avis doit contenir au maximum {max} caractères.",
877877
"plugin-review:error-downvote-comment": "Indiquez au développeur ce qui pourrait être amélioré.",
878878
"plugin-review:error-empty": "Ajoutez un commentaire ou choisissez J’aime.",
879-
"plugin-review:unrated": "Non évalué"
879+
"plugin-review:unrated": "Non évalué",
880+
"wrapped line indent": "Word wrap indentation",
881+
"wrap-indent-same": "Same",
882+
"wrap-indent-indent": "Indent (+1 level)",
883+
"wrap-indent-deep": "Deep indent (+2 levels)",
884+
"settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size."
880885
}

‎src/lang/he-il.json‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -876,5 +876,10 @@
876876
"plugin-review:error-max-length": "הביקורת צריכה להכיל עד {max} תווים.",
877877
"plugin-review:error-downvote-comment": "ספר למפתח מה אפשר לשפר.",
878878
"plugin-review:error-empty": "הוסף תגובה או בחר באהבתי.",
879-
"plugin-review:unrated": "ללא דירוג"
879+
"plugin-review:unrated": "ללא דירוג",
880+
"wrapped line indent": "Word wrap indentation",
881+
"wrap-indent-same": "Same",
882+
"wrap-indent-indent": "Indent (+1 level)",
883+
"wrap-indent-deep": "Deep indent (+2 levels)",
884+
"settings-info-editor-wrapping-indent": "Choose whether wrapped text starts at the left edge, matches the original line's indentation, or is indented further. Each extra level uses your tab size."
880885
}

0 commit comments

Comments
 (0)