├── .editorconfig ├── .eslintignore ├── .eslintrc.cjs ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .releaserc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets ├── all-options.png └── basic-input.png ├── babel.config.cjs ├── commitlint.config.cjs ├── jest.config.cjs ├── lint-staged.config.cjs ├── package-lock.json ├── package.config.ts ├── package.json ├── renovate.json ├── sanity.config.ts ├── sanity.json ├── src ├── CodeInput.tsx ├── LanguageField.tsx ├── LanguageInput.tsx ├── PreviewCode.tsx ├── codemirror │ ├── CodeMirrorProxy.tsx │ ├── CodeModeContext.tsx │ ├── defaultCodeModes.ts │ ├── extensions │ │ ├── backwardsCompatibleTone.ts │ │ ├── highlightLineExtension.ts │ │ ├── theme.ts │ │ ├── useCodeMirrorTheme.ts │ │ └── useFontSize.ts │ ├── useCodeMirror-client.test.tsx │ ├── useCodeMirror-server.test.tsx │ ├── useCodeMirror.tsx │ └── useLanguageMode.tsx ├── config.ts ├── getMedia.tsx ├── index.ts ├── plugin.tsx ├── schema.tsx ├── types.ts ├── ui │ └── focusRingStyle.ts └── useFieldMember.ts ├── test └── schema.ts ├── tsconfig.json ├── tsconfig.lib.json ├── tsconfig.settings.json └── v2-incompatible.js /.editorconfig: -------------------------------------------------------------------------------- 1 | ; editorconfig.org 2 | root = true 3 | charset= utf8 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .workshop 2 | .eslintrc.js 3 | babel.config.js 4 | commitlint.config.js 5 | jest.config.js 6 | lib 7 | lint-staged.config.js 8 | package.config.ts 9 | v2-incompatible.js 10 | workshop.config.ts 11 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | node: false, 5 | jest: true, 6 | }, 7 | extends: [ 8 | 'sanity', 9 | 'sanity/react', // must come before sanity/typescript 10 | 'sanity/typescript', 11 | 'plugin:react/jsx-runtime', 12 | 'plugin:react-hooks/recommended', 13 | 'plugin:prettier/recommended', 14 | ], 15 | overrides: [ 16 | { 17 | files: ['*.{ts,tsx}'], 18 | }, 19 | ], 20 | parser: '@typescript-eslint/parser', 21 | parserOptions: { 22 | ecmaFeatures: { 23 | jsx: true, 24 | }, 25 | project: './tsconfig.json', 26 | }, 27 | plugins: ['prettier'], 28 | rules: { 29 | '@typescript-eslint/explicit-function-return-type': 0, 30 | '@typescript-eslint/no-shadow': 'error', 31 | '@typescript-eslint/no-unused-vars': 1, 32 | 'no-shadow': 'off', 33 | 'react/display-name': 0, 34 | 'react/jsx-handler-names': 0, 35 | 'react/jsx-no-bind': 0, 36 | }, 37 | settings: { 38 | 'import/ignore': ['\\.css$', '.*node_modules.*', '.*:.*'], 39 | 'import/resolver': { 40 | node: { 41 | paths: ['src'], 42 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 43 | }, 44 | }, 45 | }, 46 | } 47 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI & Release 3 | 4 | # Workflow name based on selected inputs. Fallback to default Github naming when expression evaluates to empty string 5 | run-name: >- 6 | ${{ 7 | inputs.release && inputs.test && 'Build ➤ Test ➤ Publish to NPM' || 8 | inputs.release && !inputs.test && 'Build ➤ Skip Tests ➤ Publish to NPM' || 9 | github.event_name == 'workflow_dispatch' && inputs.test && 'Build ➤ Test' || 10 | github.event_name == 'workflow_dispatch' && !inputs.test && 'Build ➤ Skip Tests' || 11 | '' 12 | }} 13 | 14 | on: 15 | # Build on pushes branches that have a PR (including drafts) 16 | pull_request: 17 | # Build on commits pushed to branches without a PR if it's in the allowlist 18 | push: 19 | branches: [main] 20 | # https://docs.github.com/en/actions/managing-workflow-runs/manually-running-a-workflow 21 | workflow_dispatch: 22 | inputs: 23 | test: 24 | description: Run tests 25 | required: true 26 | default: true 27 | type: boolean 28 | release: 29 | description: Release new version 30 | required: true 31 | default: false 32 | type: boolean 33 | 34 | concurrency: 35 | # On PRs builds will cancel if new pushes happen before the CI completes, as it defines `github.head_ref` and gives it the name of the branch the PR wants to merge into 36 | # Otherwise `github.run_id` ensures that you can quickly merge a queue of PRs without causing tests to auto cancel on any of the commits pushed to main. 37 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 38 | cancel-in-progress: true 39 | 40 | jobs: 41 | log-the-inputs: 42 | name: Log inputs 43 | runs-on: ubuntu-latest 44 | steps: 45 | - run: | 46 | echo "Inputs: $INPUTS" 47 | env: 48 | INPUTS: ${{ toJSON(inputs) }} 49 | 50 | build: 51 | runs-on: ubuntu-latest 52 | name: Lint & Build 53 | steps: 54 | - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3 55 | - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3 56 | with: 57 | cache: npm 58 | node-version: lts/* 59 | - run: npm ci 60 | # Linting can be skipped 61 | - run: npm run lint --if-present 62 | if: github.event.inputs.test != 'false' 63 | # But not the build script, as semantic-release will crash if this command fails so it makes sense to test it early 64 | - run: npm run prepublishOnly --if-present 65 | 66 | test: 67 | needs: build 68 | # The test matrix can be skipped, in case a new release needs to be fast-tracked and tests are already passing on main 69 | if: github.event.inputs.test != 'false' 70 | runs-on: ${{ matrix.os }} 71 | name: Node.js ${{ matrix.node }} / ${{ matrix.os }} 72 | strategy: 73 | # A test failing on windows doesn't mean it'll fail on macos. It's useful to let all tests run to its completion to get the full picture 74 | fail-fast: false 75 | matrix: 76 | # Run the testing suite on each major OS with the latest LTS release of Node.js 77 | os: [macos-latest, ubuntu-latest, windows-latest] 78 | node: [lts/*] 79 | # It makes sense to also test the oldest, and latest, versions of Node.js, on ubuntu-only since it's the fastest CI runner 80 | include: 81 | - os: ubuntu-latest 82 | # Test the oldest LTS release of Node that's still receiving bugfixes and security patches, versions older than that have reached End-of-Life 83 | node: lts/-2 84 | - os: ubuntu-latest 85 | # Test the actively developed version that will become the latest LTS release next October 86 | node: current 87 | steps: 88 | # It's only necessary to do this for windows, as mac and ubuntu are sane OS's that already use LF 89 | - name: Set git to use LF 90 | if: matrix.os == 'windows-latest' 91 | run: | 92 | git config --global core.autocrlf false 93 | git config --global core.eol lf 94 | - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3 95 | - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3 96 | with: 97 | cache: npm 98 | node-version: ${{ matrix.node }} 99 | - run: npm i 100 | - run: npm test 101 | 102 | release: 103 | needs: [build, test] 104 | # only run if opt-in during workflow_dispatch 105 | if: always() && github.event.inputs.release == 'true' && needs.build.result != 'failure' && needs.test.result != 'failure' && needs.test.result != 'cancelled' 106 | runs-on: ubuntu-latest 107 | name: Semantic release 108 | steps: 109 | - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3 110 | with: 111 | # Need to fetch entire commit history to 112 | # analyze every commit since last release 113 | fetch-depth: 0 114 | - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3 115 | with: 116 | cache: npm 117 | node-version: lts/* 118 | - run: npm ci 119 | # Branches that will release new versions are defined in .releaserc.json 120 | - run: npx semantic-release 121 | # Don't allow interrupting the release step if the job is cancelled, as it can lead to an inconsistent state 122 | # e.g. git tags were pushed but it exited before `npm publish` 123 | if: always() 124 | env: 125 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 126 | NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 127 | # Re-run semantic release with rich logs if it failed to publish for easier debugging 128 | - run: npx semantic-release --dry-run --debug 129 | if: failure() 130 | env: 131 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 132 | NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 133 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # macOS finder cache file 40 | .DS_Store 41 | 42 | # VS Code settings 43 | .vscode 44 | 45 | # IntelliJ 46 | .idea 47 | *.iml 48 | 49 | # Cache 50 | .cache 51 | 52 | # Yalc 53 | .yalc 54 | yalc.lock 55 | 56 | # npm package zips 57 | *.tgz 58 | 59 | # Compiled plugin 60 | lib 61 | 62 | # Workshop runtime files 63 | .workshop 64 | 65 | # Typescript build info 66 | tsconfig.tsbuildinfo 67 | 68 | # Sanity runtime output 69 | .sanity 70 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit "" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.workshop 2 | /test 3 | /coverage 4 | .editorconfig 5 | .eslintrc 6 | .gitignore 7 | .github 8 | .prettierrc 9 | .travis.yml 10 | .nyc_output 11 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .workshop 2 | lib 3 | pnpm-lock.yaml 4 | yarn.lock 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "printWidth": 100, 4 | "bracketSpacing": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sanity/semantic-release-preset", 3 | "branches": ["main"] 4 | } 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 📓 Changelog 4 | 5 | All notable changes to this project will be documented in this file. See 6 | [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 7 | 8 | ## [5.1.2](https://github.com/sanity-io/code-input/compare/v5.1.1...v5.1.2) (2024-12-16) 9 | 10 | ### Bug Fixes 11 | 12 | - warnings from styled-components on language input render ([#112](https://github.com/sanity-io/code-input/issues/112)) ([4c71607](https://github.com/sanity-io/code-input/commit/4c71607ecadd358d55c928ef6c31ae42712dfbcc)) 13 | 14 | ## [5.1.1](https://github.com/sanity-io/code-input/compare/v5.1.0...v5.1.1) (2024-12-14) 15 | 16 | ### Bug Fixes 17 | 18 | - **deps:** upgrade `@sanity/icons` to v3 ([2870c01](https://github.com/sanity-io/code-input/commit/2870c01eae2055c1658166d4c3effff9ba7daca2)) 19 | 20 | ## [5.1.0](https://github.com/sanity-io/code-input/compare/v5.0.0...v5.1.0) (2024-12-14) 21 | 22 | ### Features 23 | 24 | - support react 19, upgrade build tooling ([#111](https://github.com/sanity-io/code-input/issues/111)) ([d9d2826](https://github.com/sanity-io/code-input/commit/d9d2826f7eec60e8e3439cb22b938fc2f2d318e8)) 25 | 26 | ## [5.0.0](https://github.com/sanity-io/code-input/compare/v4.1.4...v5.0.0) (2024-11-28) 27 | 28 | ### ⚠ BREAKING CHANGES 29 | 30 | - **deps:** Update dependency @sanity/ui to v2 (#94) 31 | 32 | ### Bug Fixes 33 | 34 | - **deps:** Update dependency @sanity/ui to v2 ([#94](https://github.com/sanity-io/code-input/issues/94)) ([7178d87](https://github.com/sanity-io/code-input/commit/7178d8737574a36407b21f44ee4feb11a48701a7)) 35 | 36 | ## [4.1.4](https://github.com/sanity-io/code-input/compare/v4.1.3...v4.1.4) (2024-04-09) 37 | 38 | ### Bug Fixes 39 | 40 | - remove incorrect autocomplete from GROQ ([#103](https://github.com/sanity-io/code-input/issues/103)) ([1f45acb](https://github.com/sanity-io/code-input/commit/1f45acb709018d273b105f870fdf5ae323d0cd38)) 41 | 42 | ## [4.1.3](https://github.com/sanity-io/code-input/compare/v4.1.2...v4.1.3) (2024-02-21) 43 | 44 | ### Bug Fixes 45 | 46 | - update dependencies ([#99](https://github.com/sanity-io/code-input/issues/99)) ([ab8634d](https://github.com/sanity-io/code-input/commit/ab8634d516b06d526528690e3e3332bad2b9d359)) 47 | 48 | ## [4.1.2](https://github.com/sanity-io/code-input/compare/v4.1.1...v4.1.2) (2023-12-15) 49 | 50 | ### Bug Fixes 51 | 52 | - **deps:** allow styled-components v6 as peer dependency ([#89](https://github.com/sanity-io/code-input/issues/89)) ([2ba3bf2](https://github.com/sanity-io/code-input/commit/2ba3bf2d8540e7e2415e08be229d030cf2b97d16)) 53 | - **deps:** update codemirror ([#52](https://github.com/sanity-io/code-input/issues/52)) ([d883481](https://github.com/sanity-io/code-input/commit/d883481c0bde01b8b27af43caac3263abcb125a3)) 54 | - **deps:** update codemirror ([#65](https://github.com/sanity-io/code-input/issues/65)) ([b866640](https://github.com/sanity-io/code-input/commit/b8666405755e582842da7995ed9929e8fc3e3571)) 55 | - **deps:** update codemirror ([#66](https://github.com/sanity-io/code-input/issues/66)) ([5d1605e](https://github.com/sanity-io/code-input/commit/5d1605e1545ad3b33a951fb471f4f70b8f46427d)) 56 | - **deps:** update dependencies (non-major) ([#32](https://github.com/sanity-io/code-input/issues/32)) ([5f2ab52](https://github.com/sanity-io/code-input/commit/5f2ab52e467aa5b7af134b49f4023b6db4b0d1a5)) 57 | - **deps:** update dependencies (non-major) to v4.20.2 ([#67](https://github.com/sanity-io/code-input/issues/67)) ([ec2dcef](https://github.com/sanity-io/code-input/commit/ec2dcef85c506fd911cea5b05274e1c78dfc6fde)) 58 | - update package description (codemirror, not ace) ([3b769e2](https://github.com/sanity-io/code-input/commit/3b769e26b3b57cc778dadb540f9007ee97a29c30)) 59 | 60 | ## [4.1.1](https://github.com/sanity-io/code-input/compare/v4.1.0...v4.1.1) (2023-05-10) 61 | 62 | ### Bug Fixes 63 | 64 | - editor should no longer crash when highlightlines are unsorted ([e2f9d98](https://github.com/sanity-io/code-input/commit/e2f9d981997c2431fb8f6ab35ffbef359862ea80)) 65 | 66 | ## [4.1.0](https://github.com/sanity-io/code-input/compare/v4.0.0...v4.1.0) (2023-03-01) 67 | 68 | ### Features 69 | 70 | - richer preview component ([5b751e9](https://github.com/sanity-io/code-input/commit/5b751e92c2079b2e69f8f9f639cdb7cd98cfa990)) 71 | 72 | ## [4.0.0](https://github.com/sanity-io/code-input/compare/v3.0.1...v4.0.0) (2023-01-16) 73 | 74 | ### ⚠ BREAKING CHANGES 75 | 76 | - api for configuring custom languages has changed. Consult the README for details. 77 | 78 | ### Features 79 | 80 | - click line-number to highlight line ([8e5cf07](https://github.com/sanity-io/code-input/commit/8e5cf072bd70a8a6526daf55423ba040eef40d43)) 81 | - replaced AceEditor with CodeMirror as code editor ([ef4fe48](https://github.com/sanity-io/code-input/commit/ef4fe48ceab4f713873b3aeebe0b611b070ff79f)) 82 | 83 | ### Bug Fixes 84 | 85 | - **deps:** applied npx @sanity/plugin-kit inject ([9ccfe69](https://github.com/sanity-io/code-input/commit/9ccfe69ae11bc48b6fdf22d1d823a82ced561a32)) 86 | - implement sanity theming ([9566cd2](https://github.com/sanity-io/code-input/commit/9566cd2dc8d2adc5c312ee5dde0044001a401028)) 87 | - improved highlighted line color ([c151e95](https://github.com/sanity-io/code-input/commit/c151e9591ffb4abf3f7bb6bb72f558d9a951737c)) 88 | - refactored default language ([2111776](https://github.com/sanity-io/code-input/commit/2111776f91cda2a472b5d1bfaba36e2fa5f2a68e)) 89 | - render focus ring ([5fc3cc1](https://github.com/sanity-io/code-input/commit/5fc3cc14b91c524026e2ae551faa5bc735150a2c)) 90 | - wrap lines ([301640e](https://github.com/sanity-io/code-input/commit/301640e7a3614cc22dd34cdff71901521f7ea67a)) 91 | 92 | ## [3.0.1](https://github.com/sanity-io/code-input/compare/v3.0.0...v3.0.1) (2022-12-06) 93 | 94 | ### Bug Fixes 95 | 96 | - preview for portable text and arrays now works in 3.0.0+ ([f4dfd0a](https://github.com/sanity-io/code-input/commit/f4dfd0a0f34039f31c76dff750ae697eb918014f)) 97 | 98 | ## [3.0.0](https://github.com/sanity-io/code-input/compare/v2.35.2...v3.0.0) (2022-11-25) 99 | 100 | ### ⚠ BREAKING CHANGES 101 | 102 | - this version does not work in Sanity Studio v2 103 | 104 | ### Features 105 | 106 | - initial Sanity Studio v3 release ([045dd4b](https://github.com/sanity-io/code-input/commit/045dd4bb51d7611c3eb1c72706539797c951ebfe)) 107 | 108 | ### Bug Fixes 109 | 110 | - @sanity/ui 1.0.0-beta.31 ([b4d6575](https://github.com/sanity-io/code-input/commit/b4d657556be578399375328080e1e21e641097cb)) 111 | - code-definition extension to intrinsics should work again ([c7b3c1f](https://github.com/sanity-io/code-input/commit/c7b3c1fe4c667302ec7aa7d34bc131d467156406)) 112 | - compiled for dev-preview.22 ([bb9a6a7](https://github.com/sanity-io/code-input/commit/bb9a6a774c310ff3f12e5df4db48a8522632912c)) 113 | - compiled for sanity 3.0.0-rc.0 ([46085b7](https://github.com/sanity-io/code-input/commit/46085b712754c418f599b837301b43b204e72f9c)) 114 | - compiled for sanity 3.0.0-rc.0 ([ccf2b61](https://github.com/sanity-io/code-input/commit/ccf2b619d2c08b21444de666ff2a3c5f67f9e476)) 115 | - **deps:** dev-preview.21 ([89ecdc3](https://github.com/sanity-io/code-input/commit/89ecdc316d652d6456ff677b338df1a6b35be92b)) 116 | - **deps:** pin dependencies ([#28](https://github.com/sanity-io/code-input/issues/28)) ([59d92d9](https://github.com/sanity-io/code-input/commit/59d92d9ce0a6ce0cd403c32dd9058e37e7ac87e4)) 117 | - **deps:** pkg-utils & @sanity/plugin-kit ([ba975f9](https://github.com/sanity-io/code-input/commit/ba975f9cfe1b1204757673b4fbdc0f1e824f07d7)) 118 | - **deps:** react-dom as peer ([bee9c70](https://github.com/sanity-io/code-input/commit/bee9c7042f9e62a48f22c9ad2d505e8fc3c70c12)) 119 | - **deps:** sanity ^3.0.0 (works with rc.3) ([764c10a](https://github.com/sanity-io/code-input/commit/764c10abef4ac376f7c1271a197cce1140cd7e32)) 120 | - **deps:** sanity 3.0.0-dev-preview.17 and ui 0.38 ([4bef2ab](https://github.com/sanity-io/code-input/commit/4bef2ab5505144da4a286c9481c12f85c61af11c)) 121 | - **deps:** update dependency @sanity/icons to v1.3.9-beta.3 ([#30](https://github.com/sanity-io/code-input/issues/30)) ([9b49361](https://github.com/sanity-io/code-input/commit/9b493612e388dfa71dfc6be0afb5b1d7306136e3)) 122 | - **deps:** update dependency ace-builds to ^1.12.5 ([#8](https://github.com/sanity-io/code-input/issues/8)) ([ae169c7](https://github.com/sanity-io/code-input/commit/ae169c75e162322d7396132448cde942511a79d7)) 123 | - **deps:** update sanity packages to v1 (major) ([#10](https://github.com/sanity-io/code-input/issues/10)) ([260a911](https://github.com/sanity-io/code-input/commit/260a911fdf3715d5d73554488595209b1aad9e65)) 124 | - lazy load AceEditor to allow server side rendering the input ([d1d173b](https://github.com/sanity-io/code-input/commit/d1d173b01d360af69d35a8f6d0708785df439639)) 125 | - makes PreviewCode also load ace async ([#11](https://github.com/sanity-io/code-input/issues/11)) ([16eb077](https://github.com/sanity-io/code-input/commit/16eb077901266b28038142408d2e74f2c20e5aec)) 126 | - preview component now uses v3 api correctly ([280b620](https://github.com/sanity-io/code-input/commit/280b6204ac7a7863d54618be8b220ae8c6b0e718)) 127 | - preview is now dev-preview.17 compatible ([58a75d9](https://github.com/sanity-io/code-input/commit/58a75d96cf8f2b51aabea6e3f573922ee5fb7827)) 128 | - setup changelog generator ([a52df38](https://github.com/sanity-io/code-input/commit/a52df3808aa8448b43b812f4b6ed3058b1f3f9bb)) 129 | - update lockfile ([cf401bb](https://github.com/sanity-io/code-input/commit/cf401bbc18b921d2e7d780448aadcc9fc48cbe2a)) 130 | - use `@sanity/semantic-release-preset` ([fb0c785](https://github.com/sanity-io/code-input/commit/fb0c785b3d8b8e38cba16157452ea517ac23e05f)) 131 | 132 | ## [3.0.0-v3-studio.15](https://github.com/sanity-io/code-input/compare/v3.0.0-v3-studio.14...v3.0.0-v3-studio.15) (2022-11-04) 133 | 134 | ### Bug Fixes 135 | 136 | - **deps:** pkg-utils & @sanity/plugin-kit ([ba975f9](https://github.com/sanity-io/code-input/commit/ba975f9cfe1b1204757673b4fbdc0f1e824f07d7)) 137 | 138 | ## [3.0.0-v3-studio.14](https://github.com/sanity-io/code-input/compare/v3.0.0-v3-studio.13...v3.0.0-v3-studio.14) (2022-11-04) 139 | 140 | ### Bug Fixes 141 | 142 | - **deps:** pin dependencies ([#28](https://github.com/sanity-io/code-input/issues/28)) ([59d92d9](https://github.com/sanity-io/code-input/commit/59d92d9ce0a6ce0cd403c32dd9058e37e7ac87e4)) 143 | - **deps:** update dependency @sanity/icons to v1.3.9-beta.3 ([#30](https://github.com/sanity-io/code-input/issues/30)) ([9b49361](https://github.com/sanity-io/code-input/commit/9b493612e388dfa71dfc6be0afb5b1d7306136e3)) 144 | - **deps:** update dependency ace-builds to ^1.12.5 ([#8](https://github.com/sanity-io/code-input/issues/8)) ([ae169c7](https://github.com/sanity-io/code-input/commit/ae169c75e162322d7396132448cde942511a79d7)) 145 | 146 | ## [3.0.0-v3-studio.13](https://github.com/sanity-io/code-input/compare/v3.0.0-v3-studio.12...v3.0.0-v3-studio.13) (2022-11-02) 147 | 148 | ### Bug Fixes 149 | 150 | - compiled for sanity 3.0.0-rc.0 ([46085b7](https://github.com/sanity-io/code-input/commit/46085b712754c418f599b837301b43b204e72f9c)) 151 | - compiled for sanity 3.0.0-rc.0 ([ccf2b61](https://github.com/sanity-io/code-input/commit/ccf2b619d2c08b21444de666ff2a3c5f67f9e476)) 152 | 153 | ## [3.0.0-v3-studio.12](https://github.com/sanity-io/code-input/compare/v3.0.0-v3-studio.11...v3.0.0-v3-studio.12) (2022-10-27) 154 | 155 | ### Bug Fixes 156 | 157 | - @sanity/ui 1.0.0-beta.31 ([b4d6575](https://github.com/sanity-io/code-input/commit/b4d657556be578399375328080e1e21e641097cb)) 158 | - compiled for dev-preview.22 ([bb9a6a7](https://github.com/sanity-io/code-input/commit/bb9a6a774c310ff3f12e5df4db48a8522632912c)) 159 | 160 | ## [3.0.0-v3-studio.11](https://github.com/sanity-io/code-input/compare/v3.0.0-v3-studio.10...v3.0.0-v3-studio.11) (2022-10-07) 161 | 162 | ### Bug Fixes 163 | 164 | - code-definition extension to intrinsics should work again ([c7b3c1f](https://github.com/sanity-io/code-input/commit/c7b3c1fe4c667302ec7aa7d34bc131d467156406)) 165 | 166 | ## [3.0.0-v3-studio.10](https://github.com/sanity-io/code-input/compare/v3.0.0-v3-studio.9...v3.0.0-v3-studio.10) (2022-10-07) 167 | 168 | ### Bug Fixes 169 | 170 | - **deps:** dev-preview.21 ([89ecdc3](https://github.com/sanity-io/code-input/commit/89ecdc316d652d6456ff677b338df1a6b35be92b)) 171 | 172 | ## [3.0.0-v3-studio.9](https://github.com/sanity-io/code-input/compare/v3.0.0-v3-studio.8...v3.0.0-v3-studio.9) (2022-09-15) 173 | 174 | ### Bug Fixes 175 | 176 | - preview is now dev-preview.17 compatible ([58a75d9](https://github.com/sanity-io/code-input/commit/58a75d96cf8f2b51aabea6e3f573922ee5fb7827)) 177 | 178 | ## [3.0.0-v3-studio.8](https://github.com/sanity-io/code-input/compare/v3.0.0-v3-studio.7...v3.0.0-v3-studio.8) (2022-09-15) 179 | 180 | ### Bug Fixes 181 | 182 | - **deps:** react-dom as peer ([bee9c70](https://github.com/sanity-io/code-input/commit/bee9c7042f9e62a48f22c9ad2d505e8fc3c70c12)) 183 | - **deps:** sanity 3.0.0-dev-preview.17 and ui 0.38 ([4bef2ab](https://github.com/sanity-io/code-input/commit/4bef2ab5505144da4a286c9481c12f85c61af11c)) 184 | 185 | # [3.0.0-v3-studio.7](https://github.com/sanity-io/code-input/compare/v3.0.0-v3-studio.6...v3.0.0-v3-studio.7) (2022-08-17) 186 | 187 | ### Bug Fixes 188 | 189 | - makes PreviewCode also load ace async ([#11](https://github.com/sanity-io/code-input/issues/11)) ([16eb077](https://github.com/sanity-io/code-input/commit/16eb077901266b28038142408d2e74f2c20e5aec)) 190 | 191 | # [3.0.0-v3-studio.6](https://github.com/sanity-io/code-input/compare/v3.0.0-v3-studio.5...v3.0.0-v3-studio.6) (2022-08-17) 192 | 193 | ### Bug Fixes 194 | 195 | - **deps:** update sanity packages to v1 (major) ([#10](https://github.com/sanity-io/code-input/issues/10)) ([260a911](https://github.com/sanity-io/code-input/commit/260a911fdf3715d5d73554488595209b1aad9e65)) 196 | 197 | # [3.0.0-v3-studio.5](https://github.com/sanity-io/code-input/compare/v3.0.0-v3-studio.4...v3.0.0-v3-studio.5) (2022-08-17) 198 | 199 | ### Bug Fixes 200 | 201 | - use `@sanity/semantic-release-preset` ([fb0c785](https://github.com/sanity-io/code-input/commit/fb0c785b3d8b8e38cba16157452ea517ac23e05f)) 202 | 203 | # [3.0.0-v3-studio.4](https://github.com/sanity-io/code-input/compare/v3.0.0-v3-studio.3...v3.0.0-v3-studio.4) (2022-08-16) 204 | 205 | ### Bug Fixes 206 | 207 | - setup changelog generator ([a52df38](https://github.com/sanity-io/code-input/commit/a52df3808aa8448b43b812f4b6ed3058b1f3f9bb)) 208 | - update lockfile ([cf401bb](https://github.com/sanity-io/code-input/commit/cf401bbc18b921d2e7d780448aadcc9fc48cbe2a)) 209 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Sanity.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @sanity/code-input 2 | 3 | > This is a **Sanity Studio v3** plugin. 4 | > For the v2 version, please refer to the [v2-branch](https://github.com/sanity-io/sanity/tree/next/packages/%40sanity/code-input). 5 | 6 | ## What is it? 7 | 8 | Code input for [Sanity](https://sanity.io/). 9 | 10 | A subset of languages and features are exposed by default. More can be added via the plugin options. 11 | 12 | ![Code input](assets/basic-input.png) 13 | 14 | Click the line numbers to toggle line highlighting. 15 | 16 | ## Installation 17 | 18 | ```sh 19 | npm install @sanity/code-input 20 | ``` 21 | 22 | ## Usage 23 | 24 | Add it as a plugin in `sanity.config.ts` (or .js): 25 | 26 | ```js 27 | import {codeInput} from '@sanity/code-input' 28 | 29 | export default defineConfig({ 30 | // ... 31 | plugins: [codeInput()], 32 | }) 33 | ``` 34 | 35 | Now you can use the `code` type in your schema types: 36 | 37 | ```js 38 | import {defineType, defineField} from 'sanity' 39 | 40 | defineType({ 41 | // [...] 42 | fields: [ 43 | defineField({ 44 | type: 'code', 45 | name: 'myCodeField', 46 | title: 'My code field', 47 | }), 48 | ], 49 | }) 50 | ``` 51 | 52 | ## Options 53 | 54 | - `language` - Default language for this code field. 55 | - `languageAlternatives` - Array of languages that should be available (se its format in the example below) 56 | - `withFilename` - Boolean option to display input field for filename 57 | 58 | ```js 59 | //...fields, 60 | defineField({ 61 | type: 'code', 62 | name: 'myCodeField', 63 | title: 'Code with all options', 64 | options: { 65 | language: 'javascript', 66 | languageAlternatives: [ 67 | {title: 'Javascript', value: 'javascript'}, 68 | {title: 'HTML', value: 'html'}, 69 | {title: 'CSS', value: 'css'}, 70 | ], 71 | withFilename: true, 72 | }, 73 | }) 74 | ``` 75 | 76 | ![Code input with all options in dark mode](assets/all-options.png) 77 | 78 | ## Add support for more languages 79 | 80 | Only a subset of languages are have syntax highlighting support by default (see full list [here](https://github.com/sanity-io/code-input/blob/main/src/codemirror/defaultCodeModes.ts)). 81 | 82 | ### Mode: Reuse an existing language 83 | 84 | Some languages are similar enough, that reusing one of the default highlighters will be "good enough". 85 | To reuse an existing language, specify mode for a value in `languageAlternatives`: 86 | 87 | ```js 88 | //...fields, 89 | defineField({ 90 | name: 'zhOnly', 91 | type: 'code', 92 | options: { 93 | language: 'zh', 94 | languageAlternatives: [ 95 | //Adds support for zh language, using sh syntax highlighting 96 | {title: 'ZH', value: 'zh', mode: 'sh'}, 97 | ], 98 | }, 99 | }) 100 | ``` 101 | 102 | ### Add more languages 103 | 104 | You can add support for additional languages, or override existing ones, by providing a `codeModes` array to the plugin. 105 | `codeModes` should be an array where each value is an object with a name and a loader function. 106 | The loader function should return a codemirror `Extension` or a `Promise` that resolves to `Extension`. 107 | 108 | The loader function will be invoked when the language is selected. 109 | 110 | For a full list of officialy code-mirror languages, see: 111 | 112 | ### Example: Add support for CodeMirror 6 language (Angular) 113 | 114 | We can add support for a [CodeMirror 6 lang package](https://github.com/orgs/codemirror/repositories?language=&q=lang-&sort=&type=all): 115 | 116 | ```js 117 | // sanity.config.js 118 | 119 | // ... in the plugins array of defineConfig, where we add the codeInput plugin 120 | codeInput({ 121 | codeModes: [ 122 | { 123 | name: 'angular', 124 | // dynamic import the angular package, and initialize the plugin after it is loaded 125 | // This way, the language is only when it is selected 126 | loader: () => import('@codemirror/lang-angular').then(({angular}) => angular()), 127 | }, 128 | ], 129 | }) 130 | ``` 131 | 132 | ```js 133 | // in a code field, you can now use rust as a language as a value, or mode 134 | defineField({ 135 | name: 'exampleRust', 136 | title: 'Example usage', 137 | type: 'code', 138 | options: { 139 | languageAlternatives: [ 140 | {title: 'Javascript', value: 'javascript'}, 141 | {title: 'Angular', value: 'angular'}, 142 | {title: 'Angular-like', value: 'angular-like', mode: 'angular'}, // uses angular highlighter 143 | ], 144 | }, 145 | }) 146 | ``` 147 | 148 | For this to work, you will have to run `npm i @codemirror/lang-angular` as this package is not included by @sanity/code-input. 149 | 150 | ### Example: Add support for CodeMirror 5 legacy language (Rust) 151 | 152 | We can add support for any [CodeMirror 5 legacy language](https://github.com/codemirror/legacy-modes/tree/main/mode) using 153 | [CodeMirror 6 StreamLanguage](https://codemirror.net/docs/ref/#language.StreamLanguage). 154 | 155 | ```js 156 | // sanity.config.js 157 | import {StreamLanguage} from '@codemirror/language' 158 | 159 | // ... in the plugins array of defineConfig, where we add the codeInput plugin 160 | codeInput({ 161 | codeModes: [ 162 | { 163 | name: 'rust', 164 | // dynamic import so the language is only be loaded on demand 165 | loader: () => 166 | import('@codemirror/legacy-modes/mode/rust').then(({rust}) => StreamLanguage.define(rust)), 167 | }, 168 | ], 169 | }) 170 | ``` 171 | 172 | ```js 173 | // in a code field, you can now use rust as a language as a value, or mode 174 | defineField({ 175 | name: 'exampleRust', 176 | title: 'Example usage', 177 | type: 'code', 178 | options: { 179 | languageAlternatives: [ 180 | {title: 'Javascript', value: 'javascript'}, 181 | {title: 'Rust', value: 'rust'}, 182 | {title: 'Rust-like', value: 'rust-like', mode: 'rust'}, // uses rust highlighter 183 | ], 184 | }, 185 | }) 186 | ``` 187 | 188 | Note: `@sanity/code-input` already includes the `@codemirror/legacy-modes` and `@codemirror/language` dependencies, 189 | so no need to install them explicitly. 190 | 191 | ## Data model 192 | 193 | ```js 194 | { 195 | _type: 'code', 196 | language: 'js', 197 | highlightedLines: [1, 2], 198 | code: 'const foo = "bar"\nconsole.log(foo.toUpperCase())\n// BAR', 199 | filename: 'available when enabled' 200 | } 201 | ``` 202 | 203 | ## Example usage in frontend (React) 204 | 205 | You can use any syntax highlighter you want - but not all of them might support highlighted lines or the syntax you've defined. 206 | 207 | As outlined above, the actual code is stored in a `code` property, so if your schema has a field called `codeExample` of type `code`, the property you'd want to pass to the highlighter would be `codeExample.code`. 208 | 209 | Here's an example using [react-refractor](https://github.com/rexxars/react-refractor): 210 | 211 | ```jsx 212 | import React from 'react' 213 | import Refractor from 'react-refractor' 214 | import js from 'refractor/lang/javascript' 215 | 216 | Refractor.registerLanguage(js) 217 | 218 | export function Code(props) { 219 | return ( 220 | 226 | ) 227 | } 228 | ``` 229 | 230 | Other syntax highlighters include: 231 | 232 | - [react-lowlight](https://github.com/rexxars/react-lowlight) 233 | - [react-syntax-highlighter](https://github.com/react-syntax-highlighter/react-syntax-highlighter) 234 | - [highlight.js](https://github.com/highlightjs/highlight.js) 235 | - [prism](https://github.com/PrismJS/prism) 236 | 237 | ## License 238 | 239 | MIT-licensed. See LICENSE. 240 | 241 | ## Develop & test 242 | 243 | This plugin uses [@sanity/plugin-kit](https://github.com/sanity-io/plugin-kit) 244 | with default configuration for build & watch scripts. 245 | 246 | See [Testing a plugin in Sanity Studio](https://github.com/sanity-io/plugin-kit#testing-a-plugin-in-sanity-studio) 247 | on how to run this plugin with hotreload in the studio. 248 | 249 | ### UI Workshop 250 | 251 | Run 252 | `workshop dev` 253 | 254 | To test the CodeMirror lazy component. 255 | 256 | ### Release new version 257 | 258 | Run ["CI & Release" workflow](https://github.com/sanity-io/code-input/actions/workflows/main.yml). 259 | Make sure to select the main branch and check "Release new version". 260 | 261 | Semantic release will only release on configured branches, so it is safe to run release on any branch. 262 | -------------------------------------------------------------------------------- /assets/all-options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanity-io/code-input/55a08f9740624d3e117242601b3190fe2fbd9190/assets/all-options.png -------------------------------------------------------------------------------- /assets/basic-input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanity-io/code-input/55a08f9740624d3e117242601b3190fe2fbd9190/assets/basic-input.png -------------------------------------------------------------------------------- /babel.config.cjs: -------------------------------------------------------------------------------- 1 | // used only by jest, .parcelrc configured to ignore this file 2 | module.exports = { 3 | presets: [ 4 | '@babel/preset-env', 5 | [ 6 | '@babel/preset-react', 7 | { 8 | runtime: 'automatic', 9 | }, 10 | ], 11 | ], 12 | } 13 | -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | } 4 | -------------------------------------------------------------------------------- /jest.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | const crypto = require('crypto') 3 | module.exports = { 4 | preset: 'ts-jest', 5 | testEnvironment: 'node', 6 | testPathIgnorePatterns: ['.yalc', 'node_modules', '.idea', 'lib', '.parcel-cache'], 7 | globals: { 8 | crypto: { 9 | getRandomValues: (arr) => crypto.randomBytes(arr.length), 10 | }, 11 | }, 12 | transform: { 13 | '^.+\\.(ts|tsx)?$': ['ts-jest', {babelConfig: true}], 14 | '^.+\\.(mjs|js|jsx)$': 'babel-jest', 15 | }, 16 | transformIgnorePatterns: ['node_modules/(?!(nanoid|uuid|get-random-values-esm))'], 17 | } 18 | -------------------------------------------------------------------------------- /lint-staged.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '**/*.{js,jsx}': ['eslint'], 3 | '**/*.{ts,tsx}': ['eslint', () => 'tsc --noEmit'], 4 | } 5 | -------------------------------------------------------------------------------- /package.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from '@sanity/pkg-utils' 2 | 3 | export default defineConfig({ 4 | legacyExports: true, 5 | dist: 'lib', 6 | tsconfig: 'tsconfig.lib.json', 7 | 8 | // Remove this block to enable strict export validation 9 | extract: { 10 | rules: { 11 | 'ae-forgotten-export': 'off', 12 | 'ae-incompatible-release-tags': 'off', 13 | 'ae-internal-missing-underscore': 'off', 14 | 'ae-missing-release-tag': 'off', 15 | }, 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sanity/code-input", 3 | "version": "5.1.2", 4 | "description": "Sanity input component for code, powered by CodeMirror", 5 | "keywords": [ 6 | "sanity", 7 | "cms", 8 | "headless", 9 | "realtime", 10 | "content", 11 | "code-input", 12 | "sanity-plugin", 13 | "code-editor" 14 | ], 15 | "homepage": "https://github.com/sanity-io/code-input#readme", 16 | "bugs": { 17 | "url": "https://github.com/sanity-io/code-input/issues" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git@github.com:sanity-io/code-input.git" 22 | }, 23 | "license": "MIT", 24 | "author": "Sanity.io ", 25 | "type": "module", 26 | "exports": { 27 | ".": { 28 | "source": "./src/index.ts", 29 | "import": "./lib/index.js", 30 | "require": "./lib/index.cjs", 31 | "default": "./lib/index.js" 32 | }, 33 | "./package.json": "./package.json" 34 | }, 35 | "main": "./lib/index.cjs", 36 | "module": "./lib/index.esm.js", 37 | "types": "./lib/index.d.ts", 38 | "files": [ 39 | "lib", 40 | "sanity.json", 41 | "src", 42 | "v2-incompatible.js" 43 | ], 44 | "sideEffects": false, 45 | "browserslist": "extends @sanity/browserslist-config", 46 | "scripts": { 47 | "build": "plugin-kit verify-package --silent && pkg-utils build --strict --check --clean", 48 | "clean": "rimraf lib", 49 | "compile": "tsc --noEmit", 50 | "dev": "sanity dev", 51 | "format": "prettier --write --cache --ignore-unknown .", 52 | "link-watch": "plugin-kit link-watch", 53 | "lint": "eslint .", 54 | "prepare": "husky install", 55 | "prepublishOnly": "npm run build", 56 | "test": "jest", 57 | "watch": "pkg-utils watch --strict" 58 | }, 59 | "dependencies": { 60 | "@codemirror/autocomplete": "^6.18.3", 61 | "@codemirror/commands": "^6.7.1", 62 | "@codemirror/lang-html": "^6.4.9", 63 | "@codemirror/lang-java": "^6.0.1", 64 | "@codemirror/lang-javascript": "^6.2.2", 65 | "@codemirror/lang-json": "^6.0.1", 66 | "@codemirror/lang-markdown": "^6.3.1", 67 | "@codemirror/lang-php": "^6.0.1", 68 | "@codemirror/lang-sql": "^6.8.0", 69 | "@codemirror/language": "^6.10.6", 70 | "@codemirror/legacy-modes": "^6.4.2", 71 | "@codemirror/search": "^6.5.8", 72 | "@codemirror/state": "^6.5.0", 73 | "@codemirror/view": "^6.35.3", 74 | "@juggle/resize-observer": "^3.4.0", 75 | "@lezer/highlight": "^1.2.1", 76 | "@sanity/icons": "^3.5.2", 77 | "@sanity/incompatible-plugin": "^1.0.4", 78 | "@sanity/ui": "^2.10.9", 79 | "@uiw/codemirror-themes": "^4.23.6", 80 | "@uiw/react-codemirror": "^4.23.6" 81 | }, 82 | "devDependencies": { 83 | "@babel/core": "^7.23.6", 84 | "@babel/preset-env": "^7.23.6", 85 | "@babel/preset-react": "^7.23.3", 86 | "@commitlint/cli": "^19.6.0", 87 | "@commitlint/config-conventional": "^19.6.0", 88 | "@sanity/pkg-utils": "^6.12.0", 89 | "@sanity/plugin-kit": "^4.0.18", 90 | "@sanity/semantic-release-preset": "^5.0.0", 91 | "@testing-library/jest-dom": "^6.1.5", 92 | "@testing-library/react": "^14.1.2", 93 | "@types/jest": "^29.5.11", 94 | "@types/react": "^18.2.45", 95 | "@types/styled-components": "^5.1.34", 96 | "@typescript-eslint/eslint-plugin": "^6.14.0", 97 | "@typescript-eslint/parser": "^6.14.0", 98 | "eslint": "^8.55.0", 99 | "eslint-config-prettier": "^9.1.0", 100 | "eslint-config-sanity": "^7.0.1", 101 | "eslint-plugin-prettier": "^5.0.1", 102 | "eslint-plugin-react": "^7.33.2", 103 | "eslint-plugin-react-hooks": "^4.6.0", 104 | "husky": "^8.0.1", 105 | "jest": "^29.7.0", 106 | "jest-environment-jsdom": "^29.7.0", 107 | "lint-staged": "^15.2.0", 108 | "npm-run-all": "^4.1.5", 109 | "prettier": "^3.1.1", 110 | "prettier-plugin-packagejson": "^2.4.7", 111 | "react": "^18.2.0", 112 | "react-dom": "^18.2.0", 113 | "react-is": "^18.2.0", 114 | "rimraf": "^5.0.5", 115 | "sanity": "^3.67.1", 116 | "semantic-release": "^24.2.0", 117 | "styled-components": "^6.1.1", 118 | "ts-jest": "^29.1.1", 119 | "typescript": "^5.3.3" 120 | }, 121 | "peerDependencies": { 122 | "react": "^18 || >=19.0.0-0", 123 | "react-dom": "^18 || >=19.0.0-0", 124 | "sanity": "^3", 125 | "styled-components": "^5.2 || ^6" 126 | }, 127 | "engines": { 128 | "node": ">=18" 129 | }, 130 | "sanityExchangeUrl": "https://www.sanity.io/plugins/code-input", 131 | "sanityPlugin": { 132 | "verifyPackage": { 133 | "babelConfig": false 134 | } 135 | }, 136 | "overrides": { 137 | "conventional-changelog-conventionalcommits": ">= 8.0.0" 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>sanity-io/renovate-config", 5 | "github>sanity-io/renovate-config:studio-v3", 6 | ":reviewer(team:ecosystem)" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /sanity.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'sanity' 2 | import {structureTool} from 'sanity/structure' 3 | 4 | import {codeInput} from './src/index' 5 | 6 | export default defineConfig({ 7 | projectId: 'ppsg7ml5', 8 | dataset: 'test', 9 | plugins: [ 10 | structureTool({ 11 | structure: (S) => S.documentTypeList('codeTest'), 12 | }), 13 | codeInput(), 14 | ], 15 | schema: { 16 | types: [ 17 | { 18 | name: 'codeTest', 19 | type: 'document', 20 | fields: [ 21 | {name: 'title', type: 'string'}, 22 | {name: 'code', type: 'code'}, 23 | ], 24 | }, 25 | ], 26 | }, 27 | tasks: { 28 | enabled: false, 29 | }, 30 | scheduledPublishing: { 31 | enabled: false, 32 | }, 33 | announcements: { 34 | enabled: false, 35 | }, 36 | beta: { 37 | create: { 38 | startInCreateEnabled: false, 39 | }, 40 | }, 41 | }) 42 | -------------------------------------------------------------------------------- /sanity.json: -------------------------------------------------------------------------------- 1 | { 2 | "parts": [ 3 | { 4 | "implements": "part:@sanity/base/sanity-root", 5 | "path": "./v2-incompatible.js" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /src/CodeInput.tsx: -------------------------------------------------------------------------------- 1 | import {Box, Card, Stack, Text} from '@sanity/ui' 2 | import {Suspense, useCallback} from 'react' 3 | import {MemberField, ObjectInputProps, RenderInputCallback, set, setIfMissing, unset} from 'sanity' 4 | import styled, {css} from 'styled-components' 5 | 6 | import {useCodeMirror} from './codemirror/useCodeMirror' 7 | import {useLanguageMode} from './codemirror/useLanguageMode' 8 | import {PATH_CODE} from './config' 9 | import {LanguageField} from './LanguageField' 10 | import {CodeInputValue, CodeSchemaType} from './types' 11 | import {focusRingBorderStyle, focusRingStyle} from './ui/focusRingStyle' 12 | import {useFieldMember} from './useFieldMember' 13 | 14 | export type {CodeInputLanguage, CodeInputValue} from './types' 15 | 16 | /** 17 | * @public 18 | */ 19 | export interface CodeInputProps extends ObjectInputProps {} 20 | 21 | const EditorContainer = styled(Card)(({theme}) => { 22 | const {focusRing, input} = theme.sanity 23 | const base = theme.sanity.color.base 24 | const color = theme.sanity.color.input 25 | const border = { 26 | color: color.default.enabled.border, 27 | width: input.border.width, 28 | } 29 | 30 | return css` 31 | --input-box-shadow: ${focusRingBorderStyle(border)}; 32 | 33 | box-shadow: var(--input-box-shadow); 34 | height: 250px; 35 | min-height: 80px; 36 | overflow-y: auto; 37 | position: relative; 38 | resize: vertical; 39 | z-index: 0; 40 | 41 | & > .cm-theme { 42 | height: 100%; 43 | } 44 | 45 | &:focus-within { 46 | --input-box-shadow: ${focusRingStyle({ 47 | base, 48 | border, 49 | focusRing, 50 | })}; 51 | } 52 | ` 53 | }) 54 | 55 | /** @public */ 56 | export function CodeInput(props: CodeInputProps) { 57 | const { 58 | members, 59 | elementProps, 60 | onChange, 61 | readOnly, 62 | renderField, 63 | renderInput, 64 | renderItem, 65 | renderPreview, 66 | schemaType: type, 67 | value, 68 | onPathFocus, 69 | } = props 70 | 71 | const languageFieldMember = useFieldMember(members, 'language') 72 | const filenameMember = useFieldMember(members, 'filename') 73 | const codeFieldMember = useFieldMember(members, 'code') 74 | 75 | const handleCodeFocus = useCallback(() => { 76 | onPathFocus(PATH_CODE) 77 | }, [onPathFocus]) 78 | 79 | const onHighlightChange = useCallback( 80 | (lines: number[]) => onChange(set(lines, ['highlightedLines'])), 81 | [onChange], 82 | ) 83 | 84 | const handleCodeChange = useCallback( 85 | (code: string) => { 86 | const path = PATH_CODE 87 | const fixedLanguage = type.options?.language 88 | 89 | onChange([ 90 | setIfMissing({_type: type.name, language: fixedLanguage}), 91 | code ? set(code, path) : unset(path), 92 | ]) 93 | }, 94 | [onChange, type], 95 | ) 96 | const {languages, language, languageMode} = useLanguageMode(props.schemaType, props.value) 97 | 98 | const CodeMirror = useCodeMirror() 99 | 100 | const renderCodeInput: RenderInputCallback = useCallback( 101 | (inputProps) => { 102 | return ( 103 | 104 | {CodeMirror && ( 105 | 108 | Loading code editor... 109 | 110 | } 111 | > 112 | 122 | 123 | )} 124 | 125 | ) 126 | }, 127 | [ 128 | CodeMirror, 129 | handleCodeChange, 130 | handleCodeFocus, 131 | onHighlightChange, 132 | languageMode, 133 | elementProps.onBlur, 134 | readOnly, 135 | value, 136 | ], 137 | ) 138 | 139 | return ( 140 | 141 | {languageFieldMember && ( 142 | 151 | )} 152 | 153 | {type.options?.withFilename && filenameMember && ( 154 | 161 | )} 162 | 163 | {codeFieldMember && ( 164 | 171 | )} 172 | 173 | ) 174 | } 175 | -------------------------------------------------------------------------------- /src/LanguageField.tsx: -------------------------------------------------------------------------------- 1 | import {useCallback} from 'react' 2 | import { 3 | FieldMember, 4 | type InputProps, 5 | MemberField, 6 | type MemberFieldProps, 7 | type PrimitiveInputElementProps, 8 | } from 'sanity' 9 | 10 | import {LanguageInput} from './LanguageInput' 11 | import type {CodeInputLanguage} from './types' 12 | 13 | export function LanguageField( 14 | props: MemberFieldProps & {member: FieldMember; language: string; languages: CodeInputLanguage[]}, 15 | ) { 16 | const {member, languages, language, renderItem, renderField, renderPreview} = props 17 | 18 | const renderInput = useCallback( 19 | ({elementProps, onChange}: Omit) => { 20 | return ( 21 | 27 | ) 28 | }, 29 | [languages, language], 30 | ) 31 | 32 | return ( 33 | 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /src/LanguageInput.tsx: -------------------------------------------------------------------------------- 1 | import {Select} from '@sanity/ui' 2 | import {type ChangeEvent, useCallback} from 'react' 3 | import {set, type StringInputProps, unset} from 'sanity' 4 | 5 | import {CodeInputLanguage} from './types' 6 | 7 | export interface LanguageInputProps { 8 | language: string 9 | languages: CodeInputLanguage[] 10 | onChange: StringInputProps['onChange'] 11 | elementProps: StringInputProps['elementProps'] 12 | } 13 | 14 | /** @internal */ 15 | export function LanguageInput(props: LanguageInputProps) { 16 | const {language, languages, onChange, elementProps} = props 17 | 18 | const handleChange = useCallback( 19 | (e: ChangeEvent) => { 20 | const newValue = e.currentTarget.value 21 | onChange(newValue ? set(newValue) : unset()) 22 | }, 23 | [onChange], 24 | ) 25 | 26 | return ( 27 | 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /src/PreviewCode.tsx: -------------------------------------------------------------------------------- 1 | import {Box, Card, Flex, Label, Text} from '@sanity/ui' 2 | import {Suspense} from 'react' 3 | import {PreviewProps} from 'sanity' 4 | import styled from 'styled-components' 5 | 6 | import {useCodeMirror} from './codemirror/useCodeMirror' 7 | import {useLanguageMode} from './codemirror/useLanguageMode' 8 | import {CodeInputValue, CodeSchemaType} from './types' 9 | 10 | const PreviewContainer = styled(Box)` 11 | position: relative; 12 | ` 13 | 14 | /** 15 | * @public 16 | */ 17 | export interface PreviewCodeProps extends PreviewProps { 18 | selection?: CodeInputValue 19 | } 20 | 21 | /** 22 | * @public 23 | */ 24 | export function PreviewCode(props: PreviewCodeProps) { 25 | const {selection, schemaType: type} = props 26 | const {languageMode} = useLanguageMode(type as CodeSchemaType, props.selection) 27 | 28 | const CodeMirror = useCodeMirror() 29 | return ( 30 | 31 | 32 | {selection?.filename || selection?.language ? ( 33 | 38 | 39 | {selection?.filename ? ( 40 | 41 | 42 | {selection.filename} 43 | 44 | 45 | ) : null} 46 | {selection?.language ? : null} 47 | 48 | 49 | ) : null} 50 | {CodeMirror && ( 51 | Loading code preview...}> 52 | 66 | 67 | )} 68 | 69 | 70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /src/codemirror/CodeMirrorProxy.tsx: -------------------------------------------------------------------------------- 1 | import type {Extension} from '@codemirror/state' 2 | import {EditorView} from '@codemirror/view' 3 | import {useRootTheme} from '@sanity/ui' 4 | import CodeMirror, {type ReactCodeMirrorProps, type ReactCodeMirrorRef} from '@uiw/react-codemirror' 5 | import {forwardRef, useCallback, useContext, useEffect, useMemo, useState} from 'react' 6 | 7 | import {CodeInputConfigContext} from './CodeModeContext' 8 | import {defaultCodeModes} from './defaultCodeModes' 9 | import { 10 | highlightLine, 11 | highlightState, 12 | setHighlightedLines, 13 | } from './extensions/highlightLineExtension' 14 | import {useThemeExtension} from './extensions/theme' 15 | import {useCodeMirrorTheme} from './extensions/useCodeMirrorTheme' 16 | import {useFontSizeExtension} from './extensions/useFontSize' 17 | 18 | export interface CodeMirrorProps extends ReactCodeMirrorProps { 19 | highlightLines?: number[] 20 | languageMode?: string 21 | onHighlightChange?: (lines: number[]) => void 22 | } 23 | 24 | /** 25 | * CodeMirrorProxy is a wrapper component around CodeMirror that we lazy load to reduce initial bundle size. 26 | * 27 | * It is also responsible for integrating any CodeMirror extensions. 28 | */ 29 | const CodeMirrorProxy = forwardRef( 30 | function CodeMirrorProxy(props, ref) { 31 | const { 32 | basicSetup: basicSetupProp, 33 | highlightLines, 34 | languageMode, 35 | onHighlightChange, 36 | readOnly, 37 | value, 38 | ...codeMirrorProps 39 | } = props 40 | 41 | const themeCtx = useRootTheme() 42 | const codeMirrorTheme = useCodeMirrorTheme() 43 | const [editorView, setEditorView] = useState(undefined) 44 | 45 | // Resolve extensions 46 | const themeExtension = useThemeExtension() 47 | const fontSizeExtension = useFontSizeExtension({fontSize: 1}) 48 | const languageExtension = useLanguageExtension(languageMode) 49 | const highlightLineExtension = useMemo( 50 | () => 51 | highlightLine({ 52 | onHighlightChange, 53 | readOnly, 54 | theme: themeCtx, 55 | }), 56 | [onHighlightChange, readOnly, themeCtx], 57 | ) 58 | 59 | const extensions = useMemo(() => { 60 | const baseExtensions = [ 61 | themeExtension, 62 | fontSizeExtension, 63 | highlightLineExtension, 64 | EditorView.lineWrapping, 65 | ] 66 | if (languageExtension) { 67 | return [...baseExtensions, languageExtension] 68 | } 69 | return baseExtensions 70 | }, [fontSizeExtension, highlightLineExtension, languageExtension, themeExtension]) 71 | 72 | useEffect(() => { 73 | if (editorView) { 74 | setHighlightedLines(editorView, highlightLines ?? []) 75 | } 76 | }, [editorView, highlightLines, value]) 77 | 78 | const initialState = useMemo(() => { 79 | return { 80 | json: { 81 | doc: value ?? '', 82 | selection: { 83 | main: 0, 84 | ranges: [{anchor: 0, head: 0}], 85 | }, 86 | highlight: highlightLines ?? [], 87 | }, 88 | fields: highlightState, 89 | } 90 | // only need to calculate this on initial render 91 | // eslint-disable-next-line react-hooks/exhaustive-deps 92 | }, []) 93 | 94 | const handleCreateEditor = useCallback((view: EditorView) => { 95 | setEditorView(view) 96 | }, []) 97 | 98 | const basicSetup = useMemo( 99 | () => 100 | basicSetupProp ?? { 101 | highlightActiveLine: false, 102 | }, 103 | [basicSetupProp], 104 | ) 105 | 106 | return ( 107 | 117 | ) 118 | }, 119 | ) 120 | 121 | function useLanguageExtension(mode?: string) { 122 | const codeConfig = useContext(CodeInputConfigContext) 123 | 124 | const [languageExtension, setLanguageExtension] = useState() 125 | 126 | useEffect(() => { 127 | const customModes = codeConfig?.codeModes ?? [] 128 | const modes = [...customModes, ...defaultCodeModes] 129 | 130 | const codeMode = modes.find((m) => m.name === mode) 131 | if (!codeMode?.loader) { 132 | console.warn( 133 | `Found no codeMode for language mode ${mode}, syntax highlighting will be disabled.`, 134 | ) 135 | } 136 | let active = true 137 | Promise.resolve(codeMode?.loader()) 138 | .then((extension) => { 139 | if (active) { 140 | setLanguageExtension(extension) 141 | } 142 | }) 143 | .catch((e) => { 144 | console.error(`Failed to load language mode ${mode}`, e) 145 | if (active) { 146 | setLanguageExtension(undefined) 147 | } 148 | }) 149 | return () => { 150 | active = false 151 | } 152 | }, [mode, codeConfig]) 153 | 154 | return languageExtension 155 | } 156 | 157 | export default CodeMirrorProxy 158 | -------------------------------------------------------------------------------- /src/codemirror/CodeModeContext.tsx: -------------------------------------------------------------------------------- 1 | import {createContext} from 'react' 2 | 3 | import type {CodeInputConfig} from '../plugin' 4 | 5 | export const CodeInputConfigContext = createContext(undefined) 6 | -------------------------------------------------------------------------------- /src/codemirror/defaultCodeModes.ts: -------------------------------------------------------------------------------- 1 | import {StreamLanguage} from '@codemirror/language' 2 | import type {Extension} from '@codemirror/state' 3 | 4 | export interface CodeMode { 5 | name: string 6 | loader: ModeLoader 7 | } 8 | export type ModeLoader = () => Promise | Extension | undefined 9 | 10 | export const defaultCodeModes: CodeMode[] = [ 11 | { 12 | name: 'groq', 13 | loader: () => 14 | import('@codemirror/lang-javascript').then(({javascriptLanguage}) => javascriptLanguage), 15 | }, 16 | { 17 | name: 'javascript', 18 | loader: () => 19 | import('@codemirror/lang-javascript').then(({javascript}) => javascript({jsx: false})), 20 | }, 21 | { 22 | name: 'jsx', 23 | loader: () => 24 | import('@codemirror/lang-javascript').then(({javascript}) => javascript({jsx: true})), 25 | }, 26 | { 27 | name: 'typescript', 28 | loader: () => 29 | import('@codemirror/lang-javascript').then(({javascript}) => 30 | javascript({jsx: false, typescript: true}), 31 | ), 32 | }, 33 | { 34 | name: 'tsx', 35 | loader: () => 36 | import('@codemirror/lang-javascript').then(({javascript}) => 37 | javascript({jsx: true, typescript: true}), 38 | ), 39 | }, 40 | {name: 'php', loader: () => import('@codemirror/lang-php').then(({php}) => php())}, 41 | {name: 'sql', loader: () => import('@codemirror/lang-sql').then(({sql}) => sql())}, 42 | { 43 | name: 'mysql', 44 | loader: () => import('@codemirror/lang-sql').then(({sql, MySQL}) => sql({dialect: MySQL})), 45 | }, 46 | {name: 'json', loader: () => import('@codemirror/lang-json').then(({json}) => json())}, 47 | { 48 | name: 'markdown', 49 | loader: () => import('@codemirror/lang-markdown').then(({markdown}) => markdown()), 50 | }, 51 | {name: 'java', loader: () => import('@codemirror/lang-java').then(({java}) => java())}, 52 | {name: 'html', loader: () => import('@codemirror/lang-html').then(({html}) => html())}, 53 | { 54 | name: 'csharp', 55 | loader: () => 56 | import('@codemirror/legacy-modes/mode/clike').then(({csharp}) => 57 | StreamLanguage.define(csharp), 58 | ), 59 | }, 60 | { 61 | name: 'sh', 62 | loader: () => 63 | import('@codemirror/legacy-modes/mode/shell').then(({shell}) => StreamLanguage.define(shell)), 64 | }, 65 | { 66 | name: 'css', 67 | loader: () => 68 | import('@codemirror/legacy-modes/mode/css').then(({css}) => StreamLanguage.define(css)), 69 | }, 70 | { 71 | name: 'scss', 72 | loader: () => 73 | import('@codemirror/legacy-modes/mode/css').then(({css}) => StreamLanguage.define(css)), 74 | }, 75 | { 76 | name: 'sass', 77 | loader: () => 78 | import('@codemirror/legacy-modes/mode/sass').then(({sass}) => StreamLanguage.define(sass)), 79 | }, 80 | { 81 | name: 'ruby', 82 | loader: () => 83 | import('@codemirror/legacy-modes/mode/ruby').then(({ruby}) => StreamLanguage.define(ruby)), 84 | }, 85 | { 86 | name: 'python', 87 | loader: () => 88 | import('@codemirror/legacy-modes/mode/python').then(({python}) => 89 | StreamLanguage.define(python), 90 | ), 91 | }, 92 | { 93 | name: 'xml', 94 | loader: () => 95 | import('@codemirror/legacy-modes/mode/xml').then(({xml}) => StreamLanguage.define(xml)), 96 | }, 97 | { 98 | name: 'yaml', 99 | loader: () => 100 | import('@codemirror/legacy-modes/mode/yaml').then(({yaml}) => StreamLanguage.define(yaml)), 101 | }, 102 | { 103 | name: 'golang', 104 | loader: () => 105 | import('@codemirror/legacy-modes/mode/go').then(({go}) => StreamLanguage.define(go)), 106 | }, 107 | {name: 'text', loader: () => undefined}, 108 | {name: 'batch', loader: () => undefined}, 109 | ] 110 | -------------------------------------------------------------------------------- /src/codemirror/extensions/backwardsCompatibleTone.ts: -------------------------------------------------------------------------------- 1 | import type {ThemeContextValue} from '@sanity/ui' 2 | 3 | /** 4 | * `@sanity/ui@v2.9` introduced two new tones; "neutral" and "suggest", 5 | * which maps to "default" and "primary" respectively in the old theme. 6 | * This function returns the "backwards compatible" tone value. 7 | * 8 | * @returns The tone value that is backwards compatible with the old theme. 9 | * @internal 10 | */ 11 | export function getBackwardsCompatibleTone( 12 | themeCtx: ThemeContextValue, 13 | ): Exclude { 14 | if (themeCtx.tone !== 'neutral' && themeCtx.tone !== 'suggest') { 15 | return themeCtx.tone 16 | } 17 | 18 | return themeCtx.tone === 'neutral' ? 'default' : 'primary' 19 | } 20 | -------------------------------------------------------------------------------- /src/codemirror/extensions/highlightLineExtension.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | 3 | import {type Extension, StateEffect, StateField} from '@codemirror/state' 4 | import {Decoration, type DecorationSet, EditorView, lineNumbers} from '@codemirror/view' 5 | import type {ThemeContextValue} from '@sanity/ui' 6 | import {rgba} from '@sanity/ui/theme' 7 | 8 | import {getBackwardsCompatibleTone} from './backwardsCompatibleTone' 9 | 10 | const highlightLineClass = 'cm-highlight-line' 11 | 12 | export const addLineHighlight = StateEffect.define() 13 | export const removeLineHighlight = StateEffect.define() 14 | 15 | export const lineHighlightField = StateField.define({ 16 | create() { 17 | return Decoration.none 18 | }, 19 | update(lines, tr) { 20 | lines = lines.map(tr.changes) 21 | for (const e of tr.effects) { 22 | if (e.is(addLineHighlight)) { 23 | lines = lines.update({add: [lineHighlightMark.range(e.value)]}) 24 | } 25 | if (e.is(removeLineHighlight)) { 26 | lines = lines.update({ 27 | filter: (from) => { 28 | // removeLineHighlight value is lineStart for the highlight, so keep other effects 29 | return from !== e.value 30 | }, 31 | }) 32 | } 33 | } 34 | return lines 35 | }, 36 | toJSON(value, state) { 37 | const highlightLines: number[] = [] 38 | const iter = value.iter() 39 | while (iter.value) { 40 | const lineNumber = state.doc.lineAt(iter.from).number 41 | if (!highlightLines.includes(lineNumber)) { 42 | highlightLines.push(lineNumber) 43 | } 44 | iter.next() 45 | } 46 | return highlightLines 47 | }, 48 | fromJSON(value: number[], state) { 49 | const lines = state.doc.lines 50 | const highlights = value 51 | .filter((line) => line <= lines) // one-indexed 52 | .map((line) => lineHighlightMark.range(state.doc.line(line).from)) 53 | highlights.sort((a, b) => a.from - b.from) 54 | try { 55 | return Decoration.none.update({ 56 | add: highlights, 57 | }) 58 | } catch (e) { 59 | console.error(e) 60 | return Decoration.none 61 | } 62 | }, 63 | provide: (f) => EditorView.decorations.from(f), 64 | }) 65 | 66 | const lineHighlightMark = Decoration.line({ 67 | class: highlightLineClass, 68 | }) 69 | 70 | export const highlightState: { 71 | [prop: string]: StateField 72 | } = { 73 | highlight: lineHighlightField, 74 | } 75 | 76 | export interface HighlightLineConfig { 77 | onHighlightChange?: (lines: number[]) => void 78 | readOnly?: boolean 79 | theme: ThemeContextValue 80 | } 81 | 82 | function createCodeMirrorTheme(options: {themeCtx: ThemeContextValue}) { 83 | const {themeCtx} = options 84 | 85 | const fallbackTone = getBackwardsCompatibleTone(themeCtx) 86 | 87 | const dark = {color: themeCtx.theme.color.dark[fallbackTone]} 88 | const light = {color: themeCtx.theme.color.light[fallbackTone]} 89 | 90 | return EditorView.baseTheme({ 91 | '.cm-lineNumbers': { 92 | cursor: 'default', 93 | }, 94 | '.cm-line.cm-line': { 95 | position: 'relative', 96 | }, 97 | 98 | // need set background with pseudoelement so it does not render over selection color 99 | [`.${highlightLineClass}::before`]: { 100 | position: 'absolute', 101 | top: 0, 102 | bottom: 0, 103 | left: 0, 104 | right: 0, 105 | zIndex: -3, 106 | content: "''", 107 | boxSizing: 'border-box', 108 | }, 109 | [`&dark .${highlightLineClass}::before`]: { 110 | background: rgba(dark.color.muted.caution.pressed.bg, 0.5), 111 | }, 112 | [`&light .${highlightLineClass}::before`]: { 113 | background: rgba(light.color.muted.caution.pressed.bg, 0.75), 114 | }, 115 | }) 116 | } 117 | 118 | export const highlightLine = (config: HighlightLineConfig): Extension => { 119 | const highlightTheme = createCodeMirrorTheme({themeCtx: config.theme}) 120 | 121 | return [ 122 | lineHighlightField, 123 | config.readOnly 124 | ? [] 125 | : lineNumbers({ 126 | domEventHandlers: { 127 | mousedown: (editorView, lineInfo) => { 128 | // Determine if the line for the clicked gutter line number has highlighted state or not 129 | const line = editorView.state.doc.lineAt(lineInfo.from) 130 | let isHighlighted = false 131 | editorView.state 132 | .field(lineHighlightField) 133 | .between(line.from, line.to, (from, to, value) => { 134 | if (value) { 135 | isHighlighted = true 136 | return false // stop iteration 137 | } 138 | return undefined 139 | }) 140 | 141 | if (isHighlighted) { 142 | editorView.dispatch({effects: removeLineHighlight.of(line.from)}) 143 | } else { 144 | editorView.dispatch({effects: addLineHighlight.of(line.from)}) 145 | } 146 | if (config?.onHighlightChange) { 147 | config.onHighlightChange(editorView.state.toJSON(highlightState).highlight) 148 | } 149 | return true 150 | }, 151 | }, 152 | }), 153 | highlightTheme, 154 | ] 155 | } 156 | 157 | /** 158 | * Adds and removes highlights to the provided view using highlightLines 159 | * @param view 160 | * @param highlightLines 161 | */ 162 | export function setHighlightedLines(view: EditorView, highlightLines: number[]): void { 163 | const doc = view.state.doc 164 | const lines = doc.lines 165 | //1-based line numbers 166 | const allLineNumbers = Array.from({length: lines}, (x, i) => i + 1) 167 | view.dispatch({ 168 | effects: allLineNumbers.map((lineNumber) => { 169 | const line = doc.line(lineNumber) 170 | if (highlightLines?.includes(lineNumber)) { 171 | return addLineHighlight.of(line.from) 172 | } 173 | return removeLineHighlight.of(line.from) 174 | }), 175 | }) 176 | } 177 | -------------------------------------------------------------------------------- /src/codemirror/extensions/theme.ts: -------------------------------------------------------------------------------- 1 | import type {Extension} from '@codemirror/state' 2 | import {EditorView} from '@codemirror/view' 3 | import {useRootTheme} from '@sanity/ui' 4 | import {rgba} from '@sanity/ui/theme' 5 | import {useMemo} from 'react' 6 | 7 | import {getBackwardsCompatibleTone} from './backwardsCompatibleTone' 8 | 9 | export function useThemeExtension(): Extension { 10 | const themeCtx = useRootTheme() 11 | 12 | return useMemo(() => { 13 | const fallbackTone = getBackwardsCompatibleTone(themeCtx) 14 | const dark = {color: themeCtx.theme.color.dark[fallbackTone]} 15 | const light = {color: themeCtx.theme.color.light[fallbackTone]} 16 | 17 | return EditorView.baseTheme({ 18 | '&.cm-editor': { 19 | height: '100%', 20 | }, 21 | '&.cm-editor.cm-focused': { 22 | outline: 'none', 23 | }, 24 | 25 | // Matching brackets 26 | '&.cm-editor.cm-focused .cm-matchingBracket': { 27 | backgroundColor: 'transparent', 28 | }, 29 | '&.cm-editor.cm-focused .cm-nonmatchingBracket': { 30 | backgroundColor: 'transparent', 31 | }, 32 | '&dark.cm-editor.cm-focused .cm-matchingBracket': { 33 | outline: `1px solid ${dark.color.base.border}`, 34 | }, 35 | '&dark.cm-editor.cm-focused .cm-nonmatchingBracket': { 36 | outline: `1px solid ${dark.color.base.border}`, 37 | }, 38 | '&light.cm-editor.cm-focused .cm-matchingBracket': { 39 | outline: `1px solid ${light.color.base.border}`, 40 | }, 41 | '&light.cm-editor.cm-focused .cm-nonmatchingBracket': { 42 | outline: `1px solid ${light.color.base.border}`, 43 | }, 44 | 45 | // Size and padding of gutter 46 | '& .cm-lineNumbers .cm-gutterElement': { 47 | minWidth: `32px !important`, 48 | padding: `0 8px !important`, 49 | }, 50 | '& .cm-gutter.cm-foldGutter': { 51 | width: `0px !important`, 52 | }, 53 | 54 | // Color of gutter 55 | '&dark .cm-gutters': { 56 | color: `${rgba(dark.color.card.enabled.code.fg, 0.5)} !important`, 57 | borderRight: `1px solid ${rgba(dark.color.base.border, 0.5)}`, 58 | }, 59 | '&light .cm-gutters': { 60 | color: `${rgba(light.color.card.enabled.code.fg, 0.5)} !important`, 61 | borderRight: `1px solid ${rgba(light.color.base.border, 0.5)}`, 62 | }, 63 | }) 64 | }, [themeCtx]) 65 | } 66 | -------------------------------------------------------------------------------- /src/codemirror/extensions/useCodeMirrorTheme.ts: -------------------------------------------------------------------------------- 1 | import type {Extension} from '@codemirror/state' 2 | import {tags as t} from '@lezer/highlight' 3 | import {useTheme} from '@sanity/ui' 4 | import {rgba} from '@sanity/ui/theme' 5 | import {createTheme} from '@uiw/codemirror-themes' 6 | import {useMemo} from 'react' 7 | 8 | export function useCodeMirrorTheme(): Extension { 9 | const theme = useTheme() 10 | 11 | return useMemo(() => { 12 | const {code: codeFont} = theme.sanity.fonts 13 | const {base, card, dark, syntax} = theme.sanity.color 14 | 15 | return createTheme({ 16 | theme: dark ? 'dark' : 'light', 17 | settings: { 18 | background: card.enabled.bg, 19 | foreground: card.enabled.code.fg, 20 | lineHighlight: card.enabled.bg, 21 | fontFamily: codeFont.family, 22 | caret: base.focusRing, 23 | selection: rgba(base.focusRing, 0.2), 24 | selectionMatch: rgba(base.focusRing, 0.4), 25 | gutterBackground: card.disabled.bg, 26 | gutterForeground: card.disabled.code.fg, 27 | gutterActiveForeground: card.enabled.fg, 28 | }, 29 | styles: [ 30 | { 31 | tag: [t.heading, t.heading2, t.heading3, t.heading4, t.heading5, t.heading6], 32 | color: card.enabled.fg, 33 | }, 34 | {tag: t.angleBracket, color: card.enabled.code.fg}, 35 | {tag: t.atom, color: syntax.keyword}, 36 | {tag: t.attributeName, color: syntax.attrName}, 37 | {tag: t.bool, color: syntax.boolean}, 38 | {tag: t.bracket, color: card.enabled.code.fg}, 39 | {tag: t.className, color: syntax.className}, 40 | {tag: t.comment, color: syntax.comment}, 41 | {tag: t.definition(t.typeName), color: syntax.function}, 42 | { 43 | tag: [ 44 | t.definition(t.variableName), 45 | t.function(t.variableName), 46 | t.className, 47 | t.attributeName, 48 | ], 49 | color: syntax.function, 50 | }, 51 | {tag: [t.function(t.propertyName), t.propertyName], color: syntax.function}, 52 | {tag: t.keyword, color: syntax.keyword}, 53 | {tag: t.null, color: syntax.number}, 54 | {tag: t.number, color: syntax.number}, 55 | {tag: t.meta, color: card.enabled.code.fg}, 56 | {tag: t.operator, color: syntax.operator}, 57 | {tag: t.propertyName, color: syntax.property}, 58 | {tag: [t.string, t.special(t.brace)], color: syntax.string}, 59 | {tag: t.tagName, color: syntax.className}, 60 | {tag: t.typeName, color: syntax.keyword}, 61 | ], 62 | }) 63 | }, [theme]) 64 | } 65 | -------------------------------------------------------------------------------- /src/codemirror/extensions/useFontSize.ts: -------------------------------------------------------------------------------- 1 | import type {Extension} from '@codemirror/state' 2 | import {EditorView} from '@codemirror/view' 3 | import {rem, useTheme} from '@sanity/ui' 4 | import {useMemo} from 'react' 5 | 6 | export function useFontSizeExtension(props: {fontSize: number}): Extension { 7 | const {fontSize: fontSizeProp} = props 8 | const theme = useTheme() 9 | 10 | return useMemo(() => { 11 | const {code: codeFont} = theme.sanity.fonts 12 | const {fontSize, lineHeight} = codeFont.sizes[fontSizeProp] || codeFont.sizes[2] 13 | 14 | return EditorView.baseTheme({ 15 | '&': { 16 | fontSize: rem(fontSize), 17 | }, 18 | 19 | '& .cm-scroller': { 20 | lineHeight: `${lineHeight / fontSize} !important`, 21 | }, 22 | }) 23 | }, [fontSizeProp, theme]) 24 | } 25 | -------------------------------------------------------------------------------- /src/codemirror/useCodeMirror-client.test.tsx: -------------------------------------------------------------------------------- 1 | /** @jest-environment jsdom */ 2 | 3 | import {studioTheme, ThemeProvider} from '@sanity/ui' 4 | import {act, render} from '@testing-library/react' 5 | import {Suspense} from 'react' 6 | 7 | import {useCodeMirror} from './useCodeMirror' 8 | 9 | describe('useCodeMirror - client', () => { 10 | let rafMock: jest.SpyInstance 11 | 12 | beforeEach(() => { 13 | rafMock = jest 14 | .spyOn(window, 'requestAnimationFrame') 15 | .mockImplementation((callback: FrameRequestCallback): number => { 16 | try { 17 | // eslint-disable-next-line callback-return 18 | callback(0) 19 | } catch (e) { 20 | // CodeMirror does some mesurement shenanigance that json dont support 21 | // we just let it crash silently 22 | } 23 | return 0 24 | }) 25 | }) 26 | 27 | afterEach(() => { 28 | rafMock.mockRestore() 29 | }) 30 | 31 | it('should render suspended codemirror editor', async () => { 32 | const TestComponent = () => { 33 | const CodeMirror = useCodeMirror() 34 | return ( 35 | 36 | {CodeMirror && ( 37 | 38 | 39 | 40 | )} 41 | 42 | ) 43 | } 44 | let container: HTMLElement | undefined 45 | await act(async () => { 46 | const result = render() 47 | container = result.container 48 | }) 49 | expect(container).toBeTruthy() 50 | expect(container!.querySelector('.cm-theme')).toBeTruthy() 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /src/codemirror/useCodeMirror-server.test.tsx: -------------------------------------------------------------------------------- 1 | import {renderToString} from 'react-dom/server' 2 | 3 | import {useCodeMirror} from './useCodeMirror' 4 | 5 | describe('useCodeMirror - server', () => { 6 | it('should render null to string (and not throw and Error)', () => { 7 | const TestComponent = () => { 8 | const Editor = useCodeMirror() 9 | if (!Editor) { 10 | return null 11 | } 12 | throw new Error('editor should always be null in envs without window') 13 | } 14 | const serverString = renderToString() 15 | 16 | expect(serverString).toEqual('') 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /src/codemirror/useCodeMirror.tsx: -------------------------------------------------------------------------------- 1 | import {lazy, useEffect, useState} from 'react' 2 | 3 | export const CodeMirrorProxy = lazy(() => import('./CodeMirrorProxy')) 4 | 5 | export function useCodeMirror() { 6 | const [mounted, setMounted] = useState(false) 7 | useEffect(() => { 8 | requestAnimationFrame(() => setMounted(true)) 9 | }, []) 10 | 11 | return mounted ? CodeMirrorProxy : null 12 | } 13 | -------------------------------------------------------------------------------- /src/codemirror/useLanguageMode.tsx: -------------------------------------------------------------------------------- 1 | import {useMemo} from 'react' 2 | 3 | import {LANGUAGE_ALIASES, SUPPORTED_LANGUAGES} from '../config' 4 | import type {CodeInputLanguage, CodeInputValue, CodeSchemaType} from '../types' 5 | 6 | export const defaultLanguageMode = 'text' 7 | 8 | export function useLanguageMode( 9 | schemaType: CodeSchemaType, 10 | value?: CodeInputValue, 11 | ): { 12 | language: string 13 | languageMode: string 14 | languages: CodeInputLanguage[] 15 | } { 16 | const languages = useLanguageAlternatives(schemaType) 17 | const fixedLanguage = schemaType.options?.language 18 | const language = value?.language ?? fixedLanguage ?? defaultLanguageMode 19 | 20 | // the language config from the schema 21 | const configured = languages.find((entry) => entry.value === language) 22 | const languageMode = configured?.mode ?? resolveAliasedLanguage(language) ?? defaultLanguageMode 23 | 24 | return {language, languageMode, languages} 25 | } 26 | 27 | function resolveAliasedLanguage(lang?: string) { 28 | return (lang && LANGUAGE_ALIASES[lang]) ?? lang 29 | } 30 | 31 | function useLanguageAlternatives(type: CodeSchemaType) { 32 | return useMemo((): CodeInputLanguage[] => { 33 | const languageAlternatives = type.options?.languageAlternatives 34 | if (!languageAlternatives) { 35 | return SUPPORTED_LANGUAGES 36 | } 37 | 38 | if (!Array.isArray(languageAlternatives)) { 39 | throw new Error( 40 | `'options.languageAlternatives' should be an array, got ${typeof languageAlternatives}`, 41 | ) 42 | } 43 | 44 | return languageAlternatives.reduce((acc: CodeInputLanguage[], {title, value: val, mode}) => { 45 | const alias = LANGUAGE_ALIASES[val] 46 | if (alias) { 47 | // eslint-disable-next-line no-console 48 | console.warn( 49 | `'options.languageAlternatives' lists a language with value "%s", which is an alias of "%s" - please replace the value to read "%s"`, 50 | val, 51 | alias, 52 | alias, 53 | ) 54 | 55 | return acc.concat({title, value: alias, mode: mode}) 56 | } 57 | return acc.concat({title, value: val, mode}) 58 | }, []) 59 | }, [type]) 60 | } 61 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import {CodeInputLanguage} from './types' 2 | 3 | // NOTE: MAKE SURE THESE ALIGN WITH CODE MODES IN ./codemirror/defaultCodeModes.ts 4 | export const SUPPORTED_LANGUAGES: CodeInputLanguage[] = [ 5 | {title: 'Batch file', value: 'batchfile'}, 6 | {title: 'C#', value: 'csharp'}, 7 | {title: 'CSS', value: 'css'}, 8 | {title: 'Go', value: 'golang'}, 9 | {title: 'GROQ', value: 'groq'}, 10 | {title: 'HTML', value: 'html'}, 11 | {title: 'Java', value: 'java'}, 12 | {title: 'JavaScript', value: 'javascript'}, 13 | {title: 'JSON', value: 'json'}, 14 | {title: 'JSX', value: 'jsx'}, 15 | {title: 'Markdown', value: 'markdown'}, 16 | {title: 'MySQL', value: 'mysql'}, 17 | {title: 'PHP', value: 'php'}, 18 | {title: 'Plain text', value: 'text'}, 19 | {title: 'Python', value: 'python'}, 20 | {title: 'Ruby', value: 'ruby'}, 21 | {title: 'SASS', value: 'sass'}, 22 | {title: 'SCSS', value: 'scss'}, 23 | {title: 'sh', value: 'sh'}, 24 | {title: 'TSX', value: 'tsx'}, 25 | {title: 'TypeScript', value: 'typescript'}, 26 | {title: 'XML', value: 'xml'}, 27 | {title: 'YAML', value: 'yaml'}, 28 | ] 29 | 30 | export const LANGUAGE_ALIASES: Record = {js: 'javascript'} 31 | 32 | export const PATH_LANGUAGE = ['language'] 33 | export const PATH_CODE = ['code'] 34 | export const PATH_FILENAME = ['filename'] 35 | -------------------------------------------------------------------------------- /src/getMedia.tsx: -------------------------------------------------------------------------------- 1 | export function getMedia(language?: string) { 2 | if (language === 'jsx') { 3 | return ( 4 | 5 | 6 | 7 | 8 | 9 | 10 | ) 11 | } 12 | 13 | if (language === 'javascript') { 14 | return ( 15 | 16 | 17 | 21 | 22 | ) 23 | } 24 | 25 | if (language === 'php') { 26 | return ( 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ) 37 | } 38 | 39 | if (language === 'json') { 40 | return ( 41 | 46 | 47 | 48 | 49 | 50 | 51 | 61 | 71 | 72 | 80 | 88 | 89 | ) 90 | } 91 | 92 | return undefined 93 | } 94 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {CodeDefinition, codeSchema, codeTypeName} from './schema' 2 | export {type CodeInput, type CodeInputProps} from './CodeInput' 3 | export {PreviewCode, type PreviewCodeProps} from './PreviewCode' 4 | export type {CodeInputLanguage, CodeInputValue, CodeOptions, CodeSchemaType} from './types' 5 | export {codeSchema, codeTypeName} 6 | export type {CodeDefinition} 7 | 8 | export {codeInput} from './plugin' 9 | -------------------------------------------------------------------------------- /src/plugin.tsx: -------------------------------------------------------------------------------- 1 | import {definePlugin} from 'sanity' 2 | 3 | import {CodeInputConfigContext} from './codemirror/CodeModeContext' 4 | import {CodeMode} from './codemirror/defaultCodeModes' 5 | import {codeSchema} from './schema' 6 | 7 | export interface CodeInputConfig { 8 | codeModes?: CodeMode[] 9 | } 10 | 11 | /** 12 | * @public 13 | */ 14 | export const codeInput = definePlugin((config) => { 15 | const codeModes = config && config.codeModes 16 | const basePlugin = { 17 | name: '@sanity/code-input', 18 | schema: {types: [codeSchema]}, 19 | } 20 | if (!codeModes) { 21 | return basePlugin 22 | } 23 | return { 24 | ...basePlugin, 25 | form: { 26 | components: { 27 | input: (props) => { 28 | if (props.id !== 'root') { 29 | return props.renderDefault(props) 30 | } 31 | return ( 32 | 33 | {props.renderDefault(props)} 34 | 35 | ) 36 | }, 37 | }, 38 | }, 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /src/schema.tsx: -------------------------------------------------------------------------------- 1 | import {CodeBlockIcon} from '@sanity/icons' 2 | import {defineType, ObjectDefinition} from 'sanity' 3 | 4 | import {CodeInput} from './CodeInput' 5 | import {getMedia} from './getMedia' 6 | import {PreviewCode} from './PreviewCode' 7 | import {CodeOptions} from './types' 8 | 9 | /** 10 | * @public 11 | */ 12 | export const codeTypeName = 'code' as const 13 | 14 | /** 15 | * @public 16 | */ 17 | export interface CodeDefinition extends Omit { 18 | type: typeof codeTypeName 19 | options?: CodeOptions 20 | } 21 | 22 | declare module '@sanity/types' { 23 | // makes type: 'code' narrow correctly when using defineType/defineField/defineArrayMember 24 | export interface IntrinsicDefinitions { 25 | code: CodeDefinition 26 | } 27 | } 28 | 29 | /** 30 | * @public 31 | */ 32 | export const codeSchema = defineType({ 33 | name: 'code', 34 | type: 'object', 35 | title: 'Code', 36 | components: {input: CodeInput, preview: PreviewCode}, 37 | icon: CodeBlockIcon, 38 | fields: [ 39 | { 40 | name: 'language', 41 | title: 'Language', 42 | type: 'string', 43 | }, 44 | { 45 | name: 'filename', 46 | title: 'Filename', 47 | type: 'string', 48 | }, 49 | { 50 | title: 'Code', 51 | name: 'code', 52 | type: 'text', 53 | }, 54 | { 55 | title: 'Highlighted lines', 56 | name: 'highlightedLines', 57 | type: 'array', 58 | of: [ 59 | { 60 | type: 'number', 61 | title: 'Highlighted line', 62 | }, 63 | ], 64 | }, 65 | ], 66 | preview: { 67 | select: { 68 | language: 'language', 69 | code: 'code', 70 | filename: 'filename', 71 | highlightedLines: 'highlightedLines', 72 | }, 73 | prepare: (value: { 74 | language?: string 75 | code?: string 76 | filename?: string 77 | highlightedLines?: number[] 78 | }) => { 79 | return { 80 | title: value.filename || (value.language || 'unknown').toUpperCase(), 81 | media: getMedia(value?.language), 82 | selection: value, 83 | } 84 | }, 85 | }, 86 | }) 87 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import {ObjectSchemaType} from 'sanity' 2 | 3 | export interface CodeInputLanguage { 4 | title: string 5 | value: string 6 | mode?: string 7 | } 8 | 9 | /** 10 | * @public 11 | */ 12 | export interface CodeInputValue { 13 | _type?: 'code' 14 | code?: string 15 | filename?: string 16 | language?: string 17 | highlightedLines?: number[] 18 | } 19 | /** 20 | * @public 21 | */ 22 | export interface CodeOptions { 23 | theme?: string 24 | darkTheme?: string 25 | languageAlternatives?: CodeInputLanguage[] 26 | language?: string 27 | withFilename?: boolean 28 | } 29 | 30 | /** 31 | * @public 32 | */ 33 | export interface CodeSchemaType extends Omit { 34 | options?: CodeOptions 35 | } 36 | -------------------------------------------------------------------------------- /src/ui/focusRingStyle.ts: -------------------------------------------------------------------------------- 1 | /** @internal */ 2 | // todo: import from @sanity/ui instead 3 | export function focusRingBorderStyle(border: {color: string; width: number}): string { 4 | return `inset 0 0 0 ${border.width}px ${border.color}` 5 | } 6 | 7 | /** @internal */ 8 | // todo: import from @sanity/ui instead 9 | export function focusRingStyle(opts: { 10 | base?: {bg: string} 11 | border?: {color: string; width: number} 12 | focusRing: {offset: number; width: number} 13 | }): string { 14 | const {base, border, focusRing} = opts 15 | const focusRingOutsetWidth = focusRing.offset + focusRing.width 16 | const focusRingInsetWidth = 0 - focusRing.offset 17 | const bgColor = base ? base.bg : 'var(--card-bg-color)' 18 | 19 | return [ 20 | focusRingInsetWidth > 0 && `inset 0 0 0 ${focusRingInsetWidth}px var(--card-focus-ring-color)`, 21 | border && focusRingBorderStyle(border), 22 | focusRingInsetWidth < 0 && `0 0 0 ${0 - focusRingInsetWidth}px ${bgColor}`, 23 | focusRingOutsetWidth > 0 && `0 0 0 ${focusRingOutsetWidth}px var(--card-focus-ring-color)`, 24 | ] 25 | .filter(Boolean) 26 | .join(',') 27 | } 28 | -------------------------------------------------------------------------------- /src/useFieldMember.ts: -------------------------------------------------------------------------------- 1 | import {useMemo} from 'react' 2 | import {FieldMember, ObjectMember} from 'sanity' 3 | 4 | /** @internal */ 5 | export function useFieldMember( 6 | members: ObjectMember[], 7 | fieldName: string, 8 | ): FieldMember | undefined { 9 | return useMemo( 10 | () => 11 | members.find( 12 | (member): member is FieldMember => member.kind === 'field' && member.name === fieldName, 13 | ), 14 | [members, fieldName], 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /test/schema.ts: -------------------------------------------------------------------------------- 1 | import {defineType} from 'sanity' 2 | 3 | const testType = defineType({ 4 | type: 'document', 5 | name: 'test', 6 | title: 'Test', 7 | fields: [ 8 | { 9 | type: 'string', 10 | name: 'title', 11 | title: 'Title', 12 | }, 13 | { 14 | type: 'code', 15 | name: 'code', 16 | title: 'Code', 17 | }, 18 | { 19 | type: 'array', 20 | name: 'content', 21 | of: [{name: 'code', type: 'code', options: {withFilename: true}}], 22 | }, 23 | ], 24 | }) 25 | 26 | export const schema = {types: [testType]} 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.settings", 3 | "include": ["./package.config.ts", "./sanity.config.ts", "./src", "./test"], 4 | "compilerOptions": { 5 | "rootDir": ".", 6 | "jsx": "react-jsx", 7 | "noEmit": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.settings", 3 | "include": ["./src"], 4 | "exclude": [ 5 | "./src/**/__fixtures__", 6 | "./src/**/__mocks__", 7 | "./src/**/*.test.ts", 8 | "./src/**/*.test.tsx" 9 | ], 10 | "compilerOptions": { 11 | "rootDir": ".", 12 | "outDir": "./lib", 13 | "jsx": "preserve", 14 | "module": "preserve", 15 | "noEmit": true, 16 | "emitDeclarationOnly": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "esnext", 5 | "module": "esnext", 6 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "downlevelIteration": true, 10 | "declaration": true, 11 | "allowSyntheticDefaultImports": true, 12 | "skipLibCheck": true, 13 | "isolatedModules": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /v2-incompatible.js: -------------------------------------------------------------------------------- 1 | const {showIncompatiblePluginDialog} = require('@sanity/incompatible-plugin') 2 | const {name, version, sanityExchangeUrl} = require('./package.json') 3 | 4 | export default showIncompatiblePluginDialog({ 5 | name: name, 6 | versions: { 7 | v3: version, 8 | v2: '^2.30.0', 9 | }, 10 | sanityExchangeUrl, 11 | }) 12 | --------------------------------------------------------------------------------