Skip to content

Commit

Permalink
fix: implement sanity theming
Browse files Browse the repository at this point in the history
  • Loading branch information
mariuslundgard authored and snorrees committed Jan 16, 2023
1 parent 441931f commit 9566cd2
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 64 deletions.
49 changes: 36 additions & 13 deletions src/codemirror/CodeMirrorProxy.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import {forwardRef, useCallback, useContext, useEffect, useMemo, useState} from 'react'
import CodeMirror, {ReactCodeMirrorProps, ReactCodeMirrorRef} from '@uiw/react-codemirror'
import {useCodeMirrorTheme} from './useCodeMirrorTheme'
import {useCodeMirrorTheme} from './extensions/useCodeMirrorTheme'
import {Extension} from '@codemirror/state'
import {CodeInputConfigContext} from './CodeModeContext'
import {defaultCodeModes} from './defaultCodeModes'
import {highlightLine, highlightState, setHighlightedLines} from './highlightLineExtension'
import {
highlightLine,
highlightState,
setHighlightedLines,
} from './extensions/highlightLineExtension'
import {EditorView} from '@codemirror/view'
import {useRootTheme} from '@sanity/ui'
import {useFontSizeExtension} from './extensions/useFontSize'
import {useThemeExtension} from './extensions/theme'

export interface CodeMirrorProps extends ReactCodeMirrorProps {
languageMode?: string
highlightLines?: number[]
languageMode?: string
onHighlightChange?: (lines: number[]) => void
}

Expand All @@ -18,34 +25,50 @@ export interface CodeMirrorProps extends ReactCodeMirrorProps {
*
* It is also responsible for integrating any CodeMirror extensions.
*/
const CodeMirrorProxy = forwardRef<ReactCodeMirrorRef, CodeMirrorProps>((props, ref) => {
const CodeMirrorProxy = forwardRef<ReactCodeMirrorRef, CodeMirrorProps>(function CodeMirrorProxy(
props,
ref
) {
const {
value,
readOnly,
basicSetup: basicSetupProp,
highlightLines,
languageMode,
onHighlightChange,
highlightLines,
readOnly,
value,
...codeMirrorProps
} = props
const theme = useCodeMirrorTheme()

const themeCtx = useRootTheme()
const codeMirrorTheme = useCodeMirrorTheme()
const [editorView, setEditorView] = useState<EditorView | undefined>(undefined)

// Resolve extensions
const themeExtension = useThemeExtension()
const fontSizeExtension = useFontSizeExtension({fontSize: 1})
const languageExtension = useLanguageExtension(languageMode)

const extensions = useMemo(() => {
const baseExtensions = [
const highlightLineExtension = useMemo(
() =>
highlightLine({
onHighlightChange,
readOnly,
theme: themeCtx,
}),
[onHighlightChange, readOnly, themeCtx]
)

const extensions = useMemo(() => {
const baseExtensions = [
themeExtension,
fontSizeExtension,
highlightLineExtension,
EditorView.lineWrapping,
]
if (languageExtension) {
return [...baseExtensions, languageExtension]
}
return baseExtensions
}, [onHighlightChange, languageExtension, readOnly])
}, [fontSizeExtension, highlightLineExtension, languageExtension, themeExtension])

useEffect(() => {
if (editorView) {
Expand Down Expand Up @@ -87,7 +110,7 @@ const CodeMirrorProxy = forwardRef<ReactCodeMirrorRef, CodeMirrorProps>((props,
value={value}
ref={ref}
extensions={extensions}
theme={theme}
theme={codeMirrorTheme}
onCreateEditor={handleCreateEditor}
initialState={initialState}
basicSetup={basicSetup}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

import {Extension, StateEffect, StateField} from '@codemirror/state'
import {Decoration, EditorView, lineNumbers} from '@codemirror/view'
import {hues} from '@sanity/color'
import {ThemeContextValue, rgba} from '@sanity/ui'

const highlightLineClass = 'cm-highlight-line'

export const addLineHighlight = StateEffect.define<number>()
export const removeLineHighlight = StateEffect.define<number>()

Expand Down Expand Up @@ -66,37 +67,45 @@ export const highlightState: {
export interface HighlightLineConfig {
onHighlightChange?: (lines: number[]) => void
readOnly?: boolean
theme: ThemeContextValue
}

const highlightTheme = EditorView.baseTheme({
'.cm-lineNumbers': {
cursor: 'pointer',
},
'.cm-line.cm-line': {
position: 'relative',
},
// need set background with pseudoelement so it does not render over selection color
[`.${highlightLineClass}::before`]: {
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
zIndex: -3,
content: "''",
boxSizing: 'border-box',
},
[`&light .${highlightLineClass}::before`]: {
borderLeft: `2px solid ${hues.yellow[200].hex}`,
background: hues.yellow[50].hex,
},
[`&dark .${highlightLineClass}::before`]: {
borderLeft: `2px solid ${hues.yellow[200].hex}`,
background: hues.yellow[900].hex,
},
})
function createCodeMirrorTheme(options: {themeCtx: ThemeContextValue}) {
const {themeCtx} = options
const dark = {color: themeCtx.theme.color.dark[themeCtx.tone]}
const light = {color: themeCtx.theme.color.light[themeCtx.tone]}

return EditorView.baseTheme({
'.cm-lineNumbers': {
cursor: 'default',
},
'.cm-line.cm-line': {
position: 'relative',
},

// need set background with pseudoelement so it does not render over selection color
[`.${highlightLineClass}::before`]: {
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
zIndex: -3,
content: "''",
boxSizing: 'border-box',
},
[`&dark .${highlightLineClass}::before`]: {
background: rgba(dark.color.muted.caution.pressed.bg, 0.5),
},
[`&light .${highlightLineClass}::before`]: {
background: rgba(light.color.muted.caution.pressed.bg, 0.75),
},
})
}

export const highlightLine = (config: HighlightLineConfig): Extension => {
const highlightTheme = createCodeMirrorTheme({themeCtx: config.theme})

return [
lineHighlightField,
config.readOnly
Expand Down Expand Up @@ -132,6 +141,7 @@ export const highlightLine = (config: HighlightLineConfig): Extension => {
highlightTheme,
]
}

/**
* Adds and removes highlights to the provided view using highlightLines
* @param view
Expand Down
61 changes: 61 additions & 0 deletions src/codemirror/extensions/theme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {Extension} from '@codemirror/state'
import {EditorView} from '@codemirror/view'
import {rgba, useRootTheme} from '@sanity/ui'
import {useMemo} from 'react'

export function useThemeExtension(): Extension {
const themeCtx = useRootTheme()

return useMemo(() => {
const dark = {color: themeCtx.theme.color.dark[themeCtx.tone]}
const light = {color: themeCtx.theme.color.light[themeCtx.tone]}

return EditorView.baseTheme({
'&.cm-editor': {
height: '100%',
},
'&.cm-editor.cm-focused': {
outline: 'none',
},

// Matching brackets
'&.cm-editor.cm-focused .cm-matchingBracket': {
backgroundColor: 'transparent',
},
'&.cm-editor.cm-focused .cm-nonmatchingBracket': {
backgroundColor: 'transparent',
},
'&dark.cm-editor.cm-focused .cm-matchingBracket': {
outline: `1px solid ${dark.color.base.border}`,
},
'&dark.cm-editor.cm-focused .cm-nonmatchingBracket': {
outline: `1px solid ${dark.color.base.border}`,
},
'&light.cm-editor.cm-focused .cm-matchingBracket': {
outline: `1px solid ${light.color.base.border}`,
},
'&light.cm-editor.cm-focused .cm-nonmatchingBracket': {
outline: `1px solid ${light.color.base.border}`,
},

// Size and padding of gutter
'& .cm-lineNumbers .cm-gutterElement': {
minWidth: `32px !important`,
padding: `0 8px !important`,
},
'& .cm-gutter.cm-foldGutter': {
width: `0px !important`,
},

// Color of gutter
'&dark .cm-gutters': {
color: `${rgba(dark.color.card.enabled.code.fg, 0.5)} !important`,
borderRight: `1px solid ${rgba(dark.color.base.border, 0.5)}`,
},
'&light .cm-gutters': {
color: `${rgba(light.color.card.enabled.code.fg, 0.5)} !important`,
borderRight: `1px solid ${rgba(light.color.base.border, 0.5)}`,
},
})
}, [themeCtx])
}
Original file line number Diff line number Diff line change
@@ -1,50 +1,62 @@
import {useTheme} from '@sanity/ui'
import {rgba, useTheme} from '@sanity/ui'
import {useMemo} from 'react'
import {createTheme} from '@uiw/codemirror-themes'
import {hues} from '@sanity/color'
import {tags as t} from '@lezer/highlight'
import {Extension} from '@codemirror/state'

export function useCodeMirrorTheme(): Extension {
const theme = useTheme()

return useMemo(() => {
const {dark, syntax, card} = theme.sanity.color
const {code: codeFont} = theme.sanity.fonts
const {base, card, dark, syntax} = theme.sanity.color

return createTheme({
theme: dark ? 'dark' : 'light',
settings: {
background: card.enabled.bg,
foreground: card.enabled.fg,
foreground: card.enabled.code.fg,
lineHighlight: card.enabled.bg,
caret: card.enabled.fg,
selection: hues.blue[dark ? 800 : 100].hex,
selectionMatch: hues.blue[dark ? 800 : 100].hex,
fontFamily: codeFont.family,
caret: base.focusRing,
selection: rgba(base.focusRing, 0.2),
selectionMatch: rgba(base.focusRing, 0.4),
gutterBackground: card.disabled.bg,
gutterForeground: card.disabled.fg,
gutterForeground: card.disabled.code.fg,
gutterActiveForeground: card.enabled.fg,
},
styles: [
{
tag: [t.heading, t.heading2, t.heading3, t.heading4, t.heading5, t.heading6],
color: hues.blue[dark ? 400 : 700].hex,
color: card.enabled.fg,
},
{tag: t.quote, color: hues.green[dark ? 400 : 700].hex},
{tag: t.comment, color: syntax.comment},
{tag: t.variableName, color: syntax.variable},
{tag: [t.string, t.special(t.brace)], color: syntax.string},
{tag: t.number, color: syntax.number},
{tag: t.angleBracket, color: card.enabled.code.fg},
{tag: t.atom, color: syntax.keyword},
{tag: t.attributeName, color: syntax.attrName},
{tag: t.bool, color: syntax.boolean},
{tag: t.null, color: syntax.number},
{tag: t.keyword, color: syntax.keyword},
{tag: t.operator, color: syntax.operator},
{tag: t.bracket, color: card.enabled.code.fg},
{tag: t.className, color: syntax.className},
{tag: t.comment, color: syntax.comment},
{tag: t.definition(t.typeName), color: syntax.function},
{tag: t.typeName, color: syntax.entity},
{tag: t.tagName, color: syntax.className},
{tag: t.attributeName, color: syntax.attrName},
{
tag: [
t.definition(t.variableName),
t.function(t.variableName),
t.className,
t.attributeName,
],
color: syntax.function,
},
{tag: [t.function(t.propertyName), t.propertyName], color: syntax.function},
{tag: t.keyword, color: syntax.keyword},
{tag: t.null, color: syntax.number},
{tag: t.number, color: syntax.number},
{tag: t.meta, color: card.enabled.code.fg},
{tag: t.operator, color: syntax.operator},
{tag: t.propertyName, color: syntax.property},
{tag: t.meta, color: hues.gray[dark ? 400 : 700].hex},
{tag: t.angleBracket, color: hues.gray[dark ? 400 : 700].hex},
{tag: t.bracket, color: hues.gray[dark ? 400 : 700].hex},
{tag: [t.string, t.special(t.brace)], color: syntax.string},
{tag: t.tagName, color: syntax.className},
{tag: t.typeName, color: syntax.keyword},
],
})
}, [theme])
Expand Down
24 changes: 24 additions & 0 deletions src/codemirror/extensions/useFontSize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {Extension} from '@codemirror/state'
import {EditorView} from '@codemirror/view'
import {rem, useTheme} from '@sanity/ui'
import {useMemo} from 'react'

export function useFontSizeExtension(props: {fontSize: number}): Extension {
const {fontSize: fontSizeProp} = props
const theme = useTheme()

return useMemo(() => {
const {code: codeFont} = theme.sanity.fonts
const {fontSize, lineHeight} = codeFont.sizes[fontSizeProp] || codeFont.sizes[2]

return EditorView.baseTheme({
'&': {
fontSize: rem(fontSize),
},

'& .cm-scroller': {
lineHeight: `${lineHeight / fontSize} !important`,
},
})
}, [fontSizeProp, theme])
}

0 comments on commit 9566cd2

Please sign in to comment.