Skip to content

Commit 1232e76

Browse files
authored
Enable indentation with tab in ct-code-editor (commontoolsinc#1787)
* Enable indentation with tab in `ct-code-editor` * Add dependency to lock * Add more props * Fix `tabSize` setting * Enable `wordWrap` and `tabIndent` in `chatbot-note.tsx` * Format pass
1 parent f4fb95a commit 1232e76

File tree

4 files changed

+117
-2
lines changed

4 files changed

+117
-2
lines changed

deno.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/patterns/chatbot-note.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,8 @@ export default recipe<LLMTestInput, LLMTestResult>(
225225
$mentioned={mentioned}
226226
onbacklink-click={handleCharmLinkClick({})}
227227
language="text/markdown"
228-
style="min-height: 400px;"
228+
wordWrap
229+
tabIndent
229230
/>
230231
<details>
231232
<summary>Mentioned Charms</summary>

packages/ui/deno.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"test": "deno test --allow-env --allow-ffi --allow-read"
1111
},
1212
"imports": {
13+
"@codemirror/commands": "npm:@codemirror/commands@^6.8.1",
1314
"@lit/context": "npm:@lit/context@^1.1.2",
1415
"marked": "npm:marked@^4.0.0",
1516
"codemirror": "npm:codemirror@^6.0.1",

packages/ui/src/v2/components/ct-code-editor/ct-code-editor.ts

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import { BaseElement } from "../../core/base-element.ts";
33
import { styles } from "./styles.ts";
44
import { basicSetup } from "codemirror";
55
import { EditorView, keymap, placeholder } from "@codemirror/view";
6+
import { indentWithTab } from "@codemirror/commands";
67
import { Compartment, EditorState, Extension } from "@codemirror/state";
7-
import { LanguageSupport } from "@codemirror/language";
8+
import { indentUnit, LanguageSupport } from "@codemirror/language";
89
import { javascript as createJavaScript } from "@codemirror/lang-javascript";
910
import { markdown as createMarkdown } from "@codemirror/lang-markdown";
1011
import { css as createCss } from "@codemirror/lang-css";
@@ -82,6 +83,12 @@ const getLangExtFromMimeType = (mime: MimeType) => {
8283
* @attr {number} timingDelay - Delay in milliseconds for debounce/throttle (default: 500)
8384
* @attr {Array} mentionable - Array of mentionable items with Charm structure for backlink autocomplete
8485
* @attr {Array} mentioned - Optional Cell of live Charms mentioned in content
86+
* @attr {boolean} wordWrap - Enable soft line wrapping (default: true)
87+
* @attr {boolean} lineNumbers - Show line numbers gutter (default: false)
88+
* @attr {number} maxLineWidth - Optional max line width in ch units
89+
* (default: undefined)
90+
* @attr {number} tabSize - Tab size (spaces shown for a tab, default: 2)
91+
* @attr {boolean} tabIndent - Indent on Tab key (default: true)
8592
*
8693
* @fires ct-change - Fired when content changes with detail: { value, oldValue, language }
8794
* @fires ct-focus - Fired on focus
@@ -104,6 +111,12 @@ export class CTCodeEditor extends BaseElement {
104111
timingDelay: { type: Number },
105112
mentionable: { type: Array },
106113
mentioned: { type: Array },
114+
// New editor configuration props
115+
wordWrap: { type: Boolean },
116+
lineNumbers: { type: Boolean },
117+
maxLineWidth: { type: Number },
118+
tabSize: { type: Number },
119+
tabIndent: { type: Boolean },
107120
};
108121

109122
declare value: Cell<string> | string;
@@ -115,10 +128,21 @@ export class CTCodeEditor extends BaseElement {
115128
declare timingDelay: number;
116129
declare mentionable: Cell<Charm[]>;
117130
declare mentioned?: Cell<Charm[]>;
131+
declare wordWrap: boolean;
132+
declare lineNumbers: boolean;
133+
declare maxLineWidth?: number;
134+
declare tabSize: number;
135+
declare tabIndent: boolean;
118136

119137
private _editorView: EditorView | undefined;
120138
private _lang = new Compartment();
121139
private _readonly = new Compartment();
140+
private _wrap = new Compartment();
141+
private _gutters = new Compartment();
142+
private _tabSizeComp = new Compartment();
143+
private _tabIndentComp = new Compartment();
144+
private _maxLineWidthComp = new Compartment();
145+
private _indentUnitComp = new Compartment();
122146
private _cleanupFns: Array<() => void> = [];
123147
private _cellController = createStringCellController(this, {
124148
timing: {
@@ -145,6 +169,12 @@ export class CTCodeEditor extends BaseElement {
145169
this.placeholder = "";
146170
this.timingStrategy = "debounce";
147171
this.timingDelay = 500;
172+
// Defaults for new props
173+
this.wordWrap = true;
174+
this.lineNumbers = false;
175+
this.maxLineWidth = undefined;
176+
this.tabSize = 2;
177+
this.tabIndent = true;
148178
}
149179

150180
/**
@@ -472,6 +502,61 @@ export class CTCodeEditor extends BaseElement {
472502
});
473503
}
474504

505+
// Update word wrap
506+
if (changedProperties.has("wordWrap") && this._editorView) {
507+
this._editorView.dispatch({
508+
effects: this._wrap.reconfigure(
509+
this.wordWrap ? EditorView.lineWrapping : [],
510+
),
511+
});
512+
}
513+
514+
// Update line numbers visibility (hide gutters when false)
515+
if (changedProperties.has("lineNumbers") && this._editorView) {
516+
const hideGutters = !this.lineNumbers;
517+
const ext = hideGutters
518+
? EditorView.theme({
519+
".cm-gutters": { display: "none" },
520+
".cm-content": { paddingLeft: "0px" },
521+
})
522+
: [] as unknown as Extension;
523+
this._editorView.dispatch({
524+
effects: this._gutters.reconfigure(ext),
525+
});
526+
}
527+
528+
// Update tab size
529+
if (changedProperties.has("tabSize") && this._editorView) {
530+
const size = this.tabSize ?? 2;
531+
this._editorView.dispatch({
532+
effects: [
533+
this._tabSizeComp.reconfigure(EditorState.tabSize.of(size)),
534+
this._indentUnitComp.reconfigure(indentUnit.of(" ".repeat(size))),
535+
],
536+
});
537+
}
538+
539+
// Update tab indent keymap
540+
if (changedProperties.has("tabIndent") && this._editorView) {
541+
const ext = this.tabIndent ? keymap.of([indentWithTab]) : [];
542+
this._editorView.dispatch({
543+
effects: this._tabIndentComp.reconfigure(ext),
544+
});
545+
}
546+
547+
// Update max line width theme
548+
if (changedProperties.has("maxLineWidth") && this._editorView) {
549+
const n = this.maxLineWidth;
550+
const ext = typeof n === "number" && n > 0
551+
? EditorView.theme({
552+
".cm-content": { maxWidth: `${n}ch` },
553+
})
554+
: [] as unknown as Extension;
555+
this._editorView.dispatch({
556+
effects: this._maxLineWidthComp.reconfigure(ext),
557+
});
558+
}
559+
475560
// Update timing controller if timing options changed
476561
if (
477562
changedProperties.has("timingStrategy") ||
@@ -525,9 +610,35 @@ export class CTCodeEditor extends BaseElement {
525610
// Create editor extensions
526611
const extensions: Extension[] = [
527612
basicSetup,
613+
// Tab indentation keymap (toggleable)
614+
this._tabIndentComp.of(this.tabIndent ? keymap.of([indentWithTab]) : []),
528615
oneDark,
529616
this._lang.of(getLangExtFromMimeType(this.language)),
530617
this._readonly.of(EditorState.readOnly.of(this.readonly)),
618+
// Word wrapping
619+
this._wrap.of(this.wordWrap ? EditorView.lineWrapping : []),
620+
// Hide gutters when line numbers are disabled
621+
this._gutters.of(
622+
!this.lineNumbers
623+
? EditorView.theme({
624+
".cm-gutters": { display: "none" },
625+
".cm-content": { paddingLeft: "0px" },
626+
})
627+
: [] as unknown as Extension,
628+
),
629+
// Tab size
630+
this._tabSizeComp.of(EditorState.tabSize.of(this.tabSize ?? 2)),
631+
this._indentUnitComp.of(
632+
indentUnit.of(" ".repeat(this.tabSize ?? 2)),
633+
),
634+
// Optional max line width (in ch)
635+
this._maxLineWidthComp.of(
636+
typeof this.maxLineWidth === "number" && this.maxLineWidth > 0
637+
? EditorView.theme({
638+
".cm-content": { maxWidth: `${this.maxLineWidth}ch` },
639+
})
640+
: [] as unknown as Extension,
641+
),
531642
EditorView.updateListener.of((update) => {
532643
if (update.docChanged && !this.readonly) {
533644
const value = update.state.doc.toString();

0 commit comments

Comments
 (0)