Colors the prompt's draft as it is typed: its markdown, the code in its fences, and in shell mode the command.

Colors the draft in Claude Code's prompt box as it is typed, as opencode.vim does: headings, emphasis, code spans, links, lists and quotes, and the code inside a fence by the language it names. In shell mode the draft is a command and is colored as one.
The colors are Claude Code's own theme colors, so they follow whatever /theme picks, light or dark.
There is no tree-sitter in a mod, so the colors come from a scanner of its own (hooks/markdown.ts, hooks/code.ts). A draft longer than 20,000 characters is left plain.
claude plugin marketplace add thefuga/claude-x
claude plugin install syntax@claude-x
It has no options: disable it (claude plugin disable syntax@claude-x) to leave the draft plain.
:e is colored at the next typed key.The colors disappear from the whole draft when its text changes by anything other than a typed key, and come back at the next typed character. The text itself is never touched.
It happens on:
chat:newline in ~/.claude/keybindings.json, such as an Enter rebound to add a line instead of sending;x, dd, p, u;:e.Typing, pasting, backspace, moving the cursor and a visual selection keep the colors. So does a new line from Ctrl+J, in a terminal that sends it as a plain line feed.
Claude Code lets a mod paint the draft only in its answer to prompt.edit, which it raises for the keys its text field handles itself. The cases above change the draft without raising it, and Claude Code drops the colors it held for the old text. The mod cannot paint again on its own: the one call that paints a whole draft ($.prompt.fill with replace) also moves the cursor to the draft's end, and can overwrite a character typed in the meantime. Seen on Claude Code 2.1.288; the fix is the engine's to make.
hooks/register.ts 23 lines1import { atom, read } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import { paint } from './paint'
5
6// The editor's mode as the statusline mod reads it off the prompt's hint, where that mod is
7// installed: in shell mode what is typed is a command, and is colored as one. Without it every
8// draft is markdown.
9const mode = atom({ plugin: 'statusline', key: 'mode' } as const, 'INSERT')
10
11export const register: Register = on => {
12 // Every keystroke passes here with the draft it leaves, and the engine paints the runs answered in
13 // the frame it draws the text. It keeps them until the draft changes with no keystroke (a new line
14 // from a key bound to one, an edit in the vim editor's normal mode), and nothing here can paint
15 // again before the next one: the one call that paints a whole draft also moves the cursor to its end.
16 on('prompt.edit', async ($, e, next) => {
17 const edited = await next(e)
18 const runs = paint(edited.text, (await read($, mode)).startsWith('SHELL'))
19
20 return runs.length === 0 ? edited : { ...edited, decorations: [...(edited.decorations ?? []), ...runs] }
21 })
22}
23hooks/paint.ts 20 lines1import type { PromptDecoration } from 'claude-code'
2
3import { syntax } from './theme'
4import { lex } from './code'
5import { scan } from './markdown'
6
7// A draft longer than this is left as the engine draws it: every key would have it read again.
8export const MAX_PAINTED = 20_000
9
10// The runs the engine paints over a draft: its markdown, or in shell mode the command it is.
11export const paint = (text: string, isShell: boolean): PromptDecoration[] => {
12 if (text.length > MAX_PAINTED) {
13 return []
14 }
15
16 const spans = isShell ? lex(text, 'sh') : scan(text)
17
18 return spans.map(({ start, end, kind }) => ({ start, end, ...syntax[kind] }))
19}
20hooks/theme.ts 33 lines1import type { PromptDecoration } from 'claude-code'
2
3import type { Kind } from './markdown'
4
5type Style = Omit<PromptDecoration, 'start' | 'end'>
6
7// Every color is a key of Claude Code's theme, so the draft follows whatever theme is picked in
8// `/theme`, light or dark. The keys are chosen for their color in the built-in themes as much as for
9// their meaning: a heading in `warning` is yellow, not a warning.
10
11// What each part of a draft's markdown, and of the code in its fences, is painted in. A part inside
12// another is painted after it, so a mark names every style its surroundings may have set.
13export const syntax: Record<Kind, Style> = {
14 heading: { color: 'warning', bold: true },
15 marker: { color: 'subtle', bold: false, italic: false, underline: false },
16 strong: { color: 'claude', bold: true },
17 emphasis: { color: 'autoAccept', italic: true },
18 raw: { color: 'success' },
19 link: { color: 'suggestion' },
20 url: { color: 'suggestion', underline: true },
21 list: { color: 'claude', bold: true },
22 quote: { color: 'subtle', italic: true },
23 tag: { color: 'suggestion' },
24 language: { color: 'autoAccept', italic: true },
25 comment: { color: 'inactive', italic: true },
26 string: { color: 'success' },
27 number: { color: 'permission' },
28 keyword: { color: 'error' },
29 constant: { color: 'permission' },
30 call: { color: 'suggestion' },
31 property: { color: 'suggestion' },
32}
33hooks/code.ts 281 lines1// The code a draft holds, colored by the language it is said to be in: comments, strings, numbers,
2// the language's own words and the names that are called. A mod's module has no parser to load (no
3// WebAssembly, no package), so this is one pattern run over the text with no grammar behind it:
4// what it cannot tell apart it leaves uncolored.
5
6export type TokenKind = 'comment' | 'string' | 'number' | 'keyword' | 'constant' | 'call' | 'property'
7
8export type Token = { start: number; end: number; kind: TokenKind }
9
10// A language as far as it is told apart here: what is tried at each place, in this order, then its
11// names against the words it keeps for itself. A name that touches one of `joins` is a part of
12// something longer (a member, a flag, a path) and is no word of the language's, whatever it spells.
13type Language = {
14 comments?: readonly string[]
15 properties?: readonly string[]
16 strings?: readonly string[]
17 variables?: readonly string[]
18 keywords?: string
19 constants?: string
20 joins?: { before: string; after: string }
21 isCaseless?: boolean
22}
23
24type Grammar = {
25 pattern: RegExp
26 keywords: ReadonlySet<string>
27 constants: ReadonlySet<string>
28 joins: { before: string; after: string }
29 isCaseless: boolean
30}
31
32const SLASHES = String.raw`\/\/[^\n]*`
33const BLOCK = String.raw`\/\*[\s\S]*?\*\/`
34// A `#` that starts a word: `$#`, `${#name}` and `a#b` are not comments.
35const HASH = String.raw`(?<=^|\s)#[^\n]*`
36const DASHES = String.raw`--[^\n]*`
37const DOUBLE = String.raw`"(?:\\.|[^"\\\n])*"`
38const SINGLE = String.raw`'(?:\\.|[^'\\\n])*'`
39// Strings that run over lines: a template, a raw string, a triple-quoted one, a shell's quotes.
40const TEMPLATE = String.raw`\x60(?:\\[\s\S]|[^\x60\\])*\x60`
41const RAW = String.raw`\x60[^\x60]*\x60`
42const TRIPLE = String.raw`"""[\s\S]*?"""|'''[\s\S]*?'''`
43const LONG_DOUBLE = String.raw`"(?:\\[\s\S]|[^"\\])*"`
44const LONG_SINGLE = String.raw`'[^']*'`
45// One character between quotes, which is what tells a Rust character from a lifetime.
46const CHARACTER = String.raw`'(?:\\[^'\n]+|[^'\\\n])'`
47const NUMBER = String.raw`\b(?:0[xX][\da-fA-F_]+|0[bB][01_]+|\d[\d_]*(?:\.\d[\d_]*)?(?:[eE][+-]?\d+)?)\b`
48// A name, and the bracket after it when it is called (`!` first for a Rust macro).
49const NAME = String.raw`(?<word>[A-Za-z_]\w*)(?<call>!?[ \t]*\()?`
50
51const MEMBER = { before: '.$', after: '' }
52const ARGUMENT = { before: '-./$', after: '-./=' }
53
54const group = (name: string, sources: readonly string[] | undefined) =>
55 sources === undefined || sources.length === 0 ? [] : [`(?<${name}>${sources.join('|')})`]
56
57const words = (list: string | undefined) => new Set(list?.split(' ') ?? [])
58
59const grammar = ({ comments, properties, strings, variables, keywords, constants, joins, isCaseless }: Language): Grammar => ({
60 pattern: new RegExp(
61 [
62 ...group('comment', comments),
63 ...group('property', properties),
64 ...group('string', strings),
65 ...group('variable', variables),
66 `(?<number>${NUMBER})`,
67 NAME,
68 ].join('|'),
69 'gm',
70 ),
71 keywords: words(keywords),
72 constants: words(constants),
73 joins: joins ?? MEMBER,
74 isCaseless: isCaseless === true,
75})
76
77const SCRIPT = grammar({
78 comments: [SLASHES, BLOCK],
79 strings: [DOUBLE, SINGLE, TEMPLATE],
80 keywords:
81 'as async await break case catch class const continue debugger declare default delete do else enum export extends finally for from function if implements import in instanceof interface keyof let namespace new of private protected public readonly return satisfies static super switch this throw try type typeof var void while yield',
82 constants: 'true false null undefined NaN Infinity',
83})
84
85const GO = grammar({
86 comments: [SLASHES, BLOCK],
87 strings: [DOUBLE, SINGLE, RAW],
88 keywords:
89 'break case chan const continue default defer else fallthrough for func go goto if import interface map package range return select struct switch type var',
90 constants: 'true false nil iota',
91})
92
93const RUST = grammar({
94 comments: [SLASHES, BLOCK],
95 strings: [LONG_DOUBLE, CHARACTER],
96 keywords:
97 'as async await break const continue crate dyn else enum extern fn for if impl in let loop match mod move mut pub ref return self Self static struct super trait type unsafe use where while',
98 constants: 'true false None Some Ok Err',
99})
100
101const PYTHON = grammar({
102 comments: [String.raw`#[^\n]*`],
103 strings: [TRIPLE, DOUBLE, SINGLE],
104 keywords:
105 'and as assert async await break case class continue def del elif else except finally for from global if import in is lambda match nonlocal not or pass raise return self try while with yield',
106 constants: 'True False None',
107})
108
109const SHELL = grammar({
110 comments: [HASH],
111 strings: [LONG_DOUBLE, LONG_SINGLE],
112 variables: [String.raw`\$\{[^}\n]*\}|\$[A-Za-z_]\w*|\$[0-9@#?*!$-]`],
113 keywords:
114 'alias case declare do done elif else esac exit export fi for function if in local readonly return set source then unset until while',
115 joins: ARGUMENT,
116})
117
118const JSON_LIKE = grammar({
119 comments: [SLASHES, BLOCK],
120 properties: [String.raw`"(?:\\.|[^"\\\n])*"(?=[ \t]*:)`],
121 strings: [DOUBLE],
122 constants: 'true false null',
123})
124
125const YAML = grammar({
126 comments: [HASH],
127 properties: [String.raw`(?<=^[ \t]*(?:-[ \t]+)*)[\w.-]+(?=[ \t]*:(?:[ \t]|$))`],
128 strings: [DOUBLE, SINGLE],
129 constants: 'true false null',
130 joins: ARGUMENT,
131})
132
133const TOML = grammar({
134 comments: [HASH],
135 properties: [String.raw`(?<=^[ \t]*)[\w.-]+(?=[ \t]*=)`],
136 strings: [TRIPLE, DOUBLE, SINGLE],
137 constants: 'true false',
138 joins: ARGUMENT,
139})
140
141const SQL = grammar({
142 comments: [DASHES, BLOCK],
143 strings: [LONG_SINGLE, DOUBLE],
144 keywords:
145 'all alter and as asc begin between by case commit create cross delete desc distinct drop else end exists foreign from full group having in index inner insert into is join key left like limit not offset on or order outer primary references returning right rollback select set table then union update values view when where with',
146 constants: 'true false null',
147 isCaseless: true,
148})
149
150const RUBY = grammar({
151 comments: [HASH],
152 strings: [DOUBLE, SINGLE],
153 keywords:
154 'alias and begin break case class def do else elsif end ensure for if in module next not or redo require rescue retry return self super then undef unless until when while yield',
155 constants: 'true false nil',
156})
157
158const LUA = grammar({
159 comments: [DASHES],
160 strings: [DOUBLE, SINGLE],
161 keywords: 'and break do else elseif end for function goto if in local not or repeat return then until while',
162 constants: 'true false nil',
163})
164
165// C and what is written like it: one list of the words those languages share.
166const BRACES = grammar({
167 comments: [SLASHES, BLOCK],
168 strings: [DOUBLE, SINGLE],
169 keywords:
170 'abstract auto bool break case catch char class const continue default do double else enum extends final finally float for fun func if implements import in int interface let long namespace new override package private protected public return short signed sizeof static struct super switch template this throw throws try typedef typename unsigned using val var virtual void volatile while',
171 constants: 'true false null nullptr nil NULL',
172})
173
174// The names a fence gives a language, each to the grammar that reads it.
175const GRAMMARS: Record<string, Grammar> = {
176 ts: SCRIPT,
177 tsx: SCRIPT,
178 typescript: SCRIPT,
179 js: SCRIPT,
180 jsx: SCRIPT,
181 javascript: SCRIPT,
182 mjs: SCRIPT,
183 cjs: SCRIPT,
184 go: GO,
185 golang: GO,
186 rs: RUST,
187 rust: RUST,
188 py: PYTHON,
189 python: PYTHON,
190 sh: SHELL,
191 bash: SHELL,
192 zsh: SHELL,
193 shell: SHELL,
194 console: SHELL,
195 json: JSON_LIKE,
196 jsonc: JSON_LIKE,
197 json5: JSON_LIKE,
198 yaml: YAML,
199 yml: YAML,
200 toml: TOML,
201 sql: SQL,
202 rb: RUBY,
203 ruby: RUBY,
204 lua: LUA,
205 c: BRACES,
206 h: BRACES,
207 cpp: BRACES,
208 'c++': BRACES,
209 cc: BRACES,
210 java: BRACES,
211 cs: BRACES,
212 csharp: BRACES,
213 kt: BRACES,
214 kotlin: BRACES,
215 swift: BRACES,
216 php: BRACES,
217 dart: BRACES,
218 scala: BRACES,
219}
220
221const touches = (characters: string, neighbor: string) => neighbor !== '' && characters.includes(neighbor)
222
223const named = (known: Grammar, word: string, isJoined: boolean, isCalled: boolean): TokenKind | null => {
224 const name = known.isCaseless ? word.toLowerCase() : word
225
226 if (!isJoined && known.keywords.has(name)) {
227 return 'keyword'
228 }
229
230 if (!isJoined && known.constants.has(name)) {
231 return 'constant'
232 }
233
234 return isCalled ? 'call' : null
235}
236
237// The tokens of `source` in `language`, a fence's name for it; none for a language not known here.
238export const lex = (source: string, language: string): Token[] => {
239 const known = GRAMMARS[language.toLowerCase()]
240
241 if (known === undefined) {
242 return []
243 }
244
245 return [...source.matchAll(known.pattern)].flatMap((match): Token[] => {
246 const { comment, property, string, variable, number, word, call } = match.groups ?? {}
247 const start = match.index
248 const whole = (kind: TokenKind) => [{ start, end: start + match[0].length, kind }]
249
250 if (comment !== undefined) {
251 return whole('comment')
252 }
253
254 if (property !== undefined) {
255 return whole('property')
256 }
257
258 if (string !== undefined) {
259 return whole('string')
260 }
261
262 if (variable !== undefined) {
263 return whole('constant')
264 }
265
266 if (number !== undefined) {
267 return whole('number')
268 }
269
270 if (word === undefined) {
271 return []
272 }
273
274 const end = start + word.length
275 const isJoined = touches(known.joins.before, source.charAt(start - 1)) || touches(known.joins.after, source.charAt(end))
276 const kind = named(known, word, isJoined, call !== undefined)
277
278 return kind === null ? [] : [{ start, end, kind }]
279 })
280}
281hooks/markdown.ts 200 lines1// The markdown a draft is written in, as far as it is worth a color: headings, emphasis, code,
2// links, lists, quotes, tags, and inside a fence the code of the language it names. Lines are read
3// one at a time against the handful of forms a prompt uses; the rest of markdown (tables, indented
4// code, reference links, a paragraph that runs into a heading) is left as plain text.
5
6import { lex } from './code'
7import type { TokenKind } from './code'
8
9export type Kind = TokenKind | 'heading' | 'marker' | 'strong' | 'emphasis' | 'raw' | 'link' | 'url' | 'list' | 'quote' | 'tag' | 'language'
10
11// One stretch of the draft and what it is. Where two cover the same text the later is the inner
12// one, and the later run is the one the engine lets win.
13export type Span = { start: number; end: number; kind: Kind }
14
15type Fence = { mark: string; language: string; body: number }
16
17// A line longer than this is a paste, not prose: its blocks are told apart and its inside left alone.
18const MAX_INLINE = 2000
19// How far emphasis is followed into emphasis.
20const MAX_DEPTH = 3
21
22const HEADING = /^( {0,3})(#{1,6})(?:[ \t]+(.*))?$/
23const QUOTE = /^( {0,3}>[ \t]?)(.*)$/
24const ITEM = /^([ \t]*)([-*+]|\d{1,9}[.)])([ \t]+)(\[[ xX]\](?=[ \t]|$))?(.*)$/
25const RULE = /^ {0,3}([-*_])(?:[ \t]*\1){2,}[ \t]*$/
26const OPENING = /^( {0,3})(`{3,}|~{3,})[ \t]*([^\s`]*)(.*)$/
27const CLOSING = /^ {0,3}(`{3,}|~{3,})[ \t]*$/
28
29// Where stars open and close emphasis. Markdown asks less of them, and would set a glob
30// (`src/**/*.ts`), a product (`2 * 3`) and a pair of star arguments in italics: here the stars stand
31// clear of a word and of a path on their outer side, and what they hold starts with neither a
32// space nor a stop and ends with neither a space nor an opening bracket.
33const OPENS = String.raw`(?<![\w*/])`
34const STARTS = String.raw`(?=[^\s*/.,;:!?)\]}])`
35const ENDS = String.raw`[^\s*/(\[{]`
36const CLOSES = String.raw`(?![\w*/])`
37
38// What is told apart inside a line, the first that fits at each place: an escaped mark, a code
39// span, a link, an address, a tag, then strong and plain emphasis. A mark inside a word is a part
40// of the word, and one with a space on its inner side opens or closes nothing.
41const INLINE = new RegExp(
42 [
43 String.raw`(?<escape>\\[!-/:-@[-\x60{-~])`,
44 String.raw`(?<ticks>\x60+)(?!\x60)(?<raw>.*?[^\x60])\k<ticks>(?!\x60)`,
45 String.raw`\[(?<label>[^\]\n]+)\]\((?<target>[^)\s]+)\)`,
46 String.raw`(?<url>https?:\/\/[^\s<>\x60]*[^\s<>\x60.,;:!?'")\]])`,
47 String.raw`(?<tag><\/?[A-Za-z][\w:-]*(?:\s[^<>\n]*)?\/?>)`,
48 String.raw`${OPENS}(?<stars>\*\*)${STARTS}(?<strong>.+?)(?<=${ENDS})\*\*${CLOSES}`,
49 String.raw`(?<![\w_])(?<bars>__)(?=\S)(?<stronger>.+?)(?<=\S)__(?![\w_])`,
50 String.raw`${OPENS}(?<star>\*)${STARTS}(?<emphasis>[^*\n]*?${ENDS})\*${CLOSES}`,
51 String.raw`(?<![\w_])(?<bar>_)(?=[^\s_])(?<emphatic>[^_\n]*?[^\s_])_(?![\w_])`,
52 ].join('|'),
53 'g',
54)
55
56const inline = (text: string, at: number, depth: number, spans: Span[]) => {
57 if (text.length > MAX_INLINE || depth > MAX_DEPTH) {
58 return
59 }
60
61 for (const match of text.matchAll(INLINE)) {
62 const { ticks, raw, label, target, url, tag, stars, strong, bars, stronger, star, emphasis, bar, emphatic } = match.groups ?? {}
63 const start = at + match.index
64 const end = start + match[0].length
65 // Marks of `width` on both sides of what they hold: the whole as `kind`, the marks over it, and
66 // what it holds read again for what is inside.
67 const wrapped = (kind: Kind, width: number, held: string) => {
68 spans.push({ start, end, kind })
69 inline(held, start + width, depth + 1, spans)
70 spans.push({ start, end: start + width, kind: 'marker' }, { start: end - width, end, kind: 'marker' })
71 }
72
73 if (ticks !== undefined && raw !== undefined) {
74 spans.push(
75 { start, end, kind: 'raw' },
76 { start, end: start + ticks.length, kind: 'marker' },
77 { start: end - ticks.length, end, kind: 'marker' },
78 )
79 } else if (label !== undefined && target !== undefined) {
80 const close = start + 1 + label.length
81
82 spans.push({ start, end, kind: 'marker' }, { start: start + 1, end: close, kind: 'link' })
83 inline(label, start + 1, depth + 1, spans)
84 spans.push({ start: close + 2, end: end - 1, kind: 'url' })
85 } else if (url !== undefined) {
86 spans.push({ start, end, kind: 'url' })
87 } else if (tag !== undefined) {
88 spans.push({ start, end, kind: 'tag' })
89 } else if (stars !== undefined && strong !== undefined) {
90 wrapped('strong', 2, strong)
91 } else if (bars !== undefined && stronger !== undefined) {
92 wrapped('strong', 2, stronger)
93 } else if (star !== undefined && emphasis !== undefined) {
94 wrapped('emphasis', 1, emphasis)
95 } else if (bar !== undefined && emphatic !== undefined) {
96 wrapped('emphasis', 1, emphatic)
97 }
98 }
99}
100
101// The fence a line opens, if it opens one. A backtick fence names no backtick after its marks,
102// which is what tells it from a line of code set in three of them.
103const opening = (line: string, at: number): Fence | null => {
104 const [, , mark, language, rest] = OPENING.exec(line) ?? []
105
106 if (mark === undefined || language === undefined || (mark.startsWith('`') && rest?.includes('`') === true)) {
107 return null
108 }
109
110 return { mark, language, body: at + line.length + 1 }
111}
112
113const closes = (line: string, { mark }: Fence) => {
114 const [, found] = CLOSING.exec(line) ?? []
115
116 return found !== undefined && found.startsWith(mark)
117}
118
119// The spans of a line that is in no fence, by the block it is.
120const block = (line: string, at: number, spans: Span[]) => {
121 const end = at + line.length
122 const heading = HEADING.exec(line)
123 const quote = QUOTE.exec(line)
124 const item = ITEM.exec(line)
125
126 if (RULE.test(line)) {
127 spans.push({ start: at, end, kind: 'marker' })
128 } else if (heading !== null) {
129 const [, indent = '', marks = '', title = ''] = heading
130 const from = at + indent.length
131
132 spans.push({ start: from, end, kind: 'heading' }, { start: from, end: from + marks.length, kind: 'marker' })
133 inline(title, end - title.length, 1, spans)
134 } else if (quote !== null) {
135 const [, lead = '', quoted = ''] = quote
136
137 spans.push({ start: at, end, kind: 'quote' }, { start: at, end: at + lead.length, kind: 'marker' })
138 inline(quoted, at + lead.length, 1, spans)
139 } else if (item !== null) {
140 const [, indent = '', bullet = '', gap = '', task, rest = ''] = item
141 const from = at + indent.length
142 const boxed = from + bullet.length + gap.length
143
144 spans.push({ start: from, end: from + bullet.length, kind: 'list' })
145
146 if (task !== undefined) {
147 spans.push({ start: boxed, end: boxed + task.length, kind: 'list' })
148 }
149
150 inline(rest, end - rest.length, 1, spans)
151 } else {
152 inline(line, at, 1, spans)
153 }
154}
155
156// The code between a fence's two lines, which ends one character before `until`, where the line
157// that closes it starts (or would, in a fence left open).
158const fenced = (text: string, { language, body }: Fence, until: number, spans: Span[]) => {
159 const source = text.slice(body, Math.max(body, until - 1))
160
161 for (const { start, end, kind } of lex(source, language)) {
162 spans.push({ start: body + start, end: body + end, kind })
163 }
164}
165
166// The spans of a draft, in the order they are to be painted.
167export const scan = (text: string): Span[] => {
168 const spans: Span[] = []
169 let open: Fence | null = null
170 let at = 0
171
172 for (const line of text.split('\n')) {
173 const end = at + line.length
174
175 if (open === null) {
176 open = opening(line, at)
177
178 if (open === null) {
179 block(line, at, spans)
180 } else {
181 const named = at + line.indexOf(open.mark) + open.mark.length
182
183 spans.push({ start: at, end: named, kind: 'marker' }, ...(named < end ? [{ start: named, end, kind: 'language' } as const] : []))
184 }
185 } else if (closes(line, open)) {
186 fenced(text, open, at, spans)
187 spans.push({ start: at, end, kind: 'marker' })
188 open = null
189 }
190
191 at = end + 1
192 }
193
194 if (open !== null) {
195 fenced(text, open, text.length + 1, spans)
196 }
197
198 return spans
199}
200types/index.d.ts 13 lines1// The editor's mode as the statusline mod's badge names it: `INSERT`, `NORMAL`, `SHELL`,
2// `SHELL NORMAL`… In shell mode the draft is colored as a command.
3export type Mode = string
4
5// The value this mod reads from the statusline mod, as that mod declares it. Nothing is kept here.
6declare module 'claude-code' {
7 interface PluginState {
8 statusline: {
9 mode: Mode
10 }
11 }
12}
13