Skip to content

Commit a7058e2

Browse files
authored
Merge pull request #4132 from udecode/fix/code-block
fix codeblock
2 parents e0b8653 + a3c81b5 commit a7058e2

File tree

13 files changed

+177
-69
lines changed

13 files changed

+177
-69
lines changed

.changeset/blue-dancers-dream.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@udecode/plate-selection': patch
3+
---
4+
5+
Fix multi-block deletion, cmd+a while selecting, arrow up/down while selecting all

.changeset/funny-ducks-care.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@udecode/plate-code-block': patch
3+
---
4+
5+
Support invalid languages

.changeset/honest-colts-fly.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@udecode/plate-core': patch
3+
---
4+
5+
Better error message for editor.api.debug.error

apps/www/content/docs/en/components/changelog.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Plate 46 - new code block
2323
- `code-syntax-leaf-static`, `code-syntax-leaf`: Updated to use lowlight token classes
2424
- Removed `prismjs` dependency and related styles
2525
- Use `lowlight` plugin option instead of `prism` option
26-
- `code-block-combobox`: add `Auto` language option
26+
- `code-block-combobox`: add `Auto` language option, change language values to match lowlight
2727
- `autoformat-plugin`: prevent autoformat on code blocks
2828

2929
```tsx

apps/www/public/r/styles/default/code-block-element.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
},
3333
{
3434
"path": "plate-ui/code-block-combobox.tsx",
35-
"content": "'use client';\n\nimport React, { useState } from 'react';\n\nimport type { TCodeBlockElement } from '@udecode/plate-code-block';\n\nimport { cn } from '@udecode/cn';\nimport { useEditorRef, useElement, useReadOnly } from '@udecode/plate/react';\nimport { Check } from 'lucide-react';\n\nimport { Button } from './button';\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from './command';\nimport { Popover, PopoverContent, PopoverTrigger } from './popover';\n\nconst languages: { label: string; value: string }[] = [\n { label: 'Auto', value: 'auto' },\n { label: 'Plain Text', value: 'plaintext' },\n { label: 'Bash', value: 'bash' },\n { label: 'C', value: 'c' },\n { label: 'C++', value: 'cpp' },\n { label: 'C#', value: 'csharp' },\n { label: 'CSS', value: 'css' },\n { label: 'Diff', value: 'diff' },\n { label: 'Go', value: 'go' },\n { label: 'GraphQL', value: 'graphql' },\n { label: 'HTML', value: 'html' },\n { label: 'Java', value: 'java' },\n { label: 'JavaScript', value: 'javascript' },\n { label: 'JSON', value: 'json' },\n { label: 'JSX', value: 'jsx' },\n { label: 'Kotlin', value: 'kotlin' },\n { label: 'Less', value: 'less' },\n { label: 'Lua', value: 'lua' },\n { label: 'Makefile', value: 'makefile' },\n { label: 'Markdown', value: 'markdown' },\n { label: 'Objective-C', value: 'objectivec' },\n { label: 'PHP', value: 'php' },\n { label: 'Python', value: 'python' },\n { label: 'R', value: 'r' },\n { label: 'Ruby', value: 'ruby' },\n { label: 'Rust', value: 'rust' },\n { label: 'SCSS', value: 'scss' },\n { label: 'Shell', value: 'shell' },\n { label: 'SQL', value: 'sql' },\n { label: 'Swift', value: 'swift' },\n { label: 'TypeScript', value: 'typescript' },\n { label: 'TSX', value: 'tsx' },\n { label: 'XML', value: 'xml' },\n { label: 'YAML', value: 'yaml' },\n];\n\nexport function CodeBlockCombobox() {\n const [open, setOpen] = useState(false);\n const readOnly = useReadOnly();\n const editor = useEditorRef();\n const element = useElement<TCodeBlockElement>();\n const value = element.lang ?? 'plaintext';\n const [searchValue, setSearchValue] = React.useState('');\n\n const items = React.useMemo(\n () =>\n languages.filter(\n (language) =>\n !searchValue ||\n language.label.toLowerCase().includes(searchValue.toLowerCase())\n ),\n [searchValue]\n );\n\n if (readOnly) return null;\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n size=\"xs\"\n variant=\"ghost\"\n className=\"h-6 justify-between text-muted-foreground gap-1 px-2 text-xs select-none\"\n aria-expanded={open}\n role=\"combobox\"\n >\n {languages.find((language) => language.value === value)?.label ??\n 'Plain Text'}\n </Button>\n </PopoverTrigger>\n <PopoverContent\n className=\"w-[200px] p-0\"\n onCloseAutoFocus={() => setSearchValue('')}\n >\n <Command shouldFilter={false}>\n <CommandInput\n className=\"h-9\"\n value={searchValue}\n onValueChange={(value) => setSearchValue(value)}\n placeholder=\"Search language...\"\n />\n <CommandEmpty>No language found.</CommandEmpty>\n\n <CommandList className=\"h-[344px] overflow-y-auto\">\n <CommandGroup>\n {items.map((language) => (\n <CommandItem\n key={language.value}\n className=\"cursor-pointer\"\n value={language.value}\n onSelect={(value) => {\n editor.tf.setNodes<TCodeBlockElement>(\n { lang: value },\n { at: element }\n );\n setSearchValue(value);\n setOpen(false);\n }}\n >\n <Check\n className={cn(\n value === language.value ? 'opacity-100' : 'opacity-0'\n )}\n />\n {language.label}\n </CommandItem>\n ))}\n </CommandGroup>\n </CommandList>\n </Command>\n </PopoverContent>\n </Popover>\n );\n}\n",
35+
"content": "'use client';\n\nimport React, { useState } from 'react';\n\nimport type { TCodeBlockElement } from '@udecode/plate-code-block';\n\nimport { cn } from '@udecode/cn';\nimport { useEditorRef, useElement, useReadOnly } from '@udecode/plate/react';\nimport { Check } from 'lucide-react';\n\nimport { Button } from './button';\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from './command';\nimport { Popover, PopoverContent, PopoverTrigger } from './popover';\n\nconst languages: { label: string; value: string }[] = [\n { label: 'Auto', value: 'auto' },\n { label: 'Plain Text', value: 'plaintext' },\n { label: 'ABAP', value: 'abap' },\n { label: 'Agda', value: 'agda' },\n { label: 'Arduino', value: 'arduino' },\n { label: 'ASCII Art', value: 'ascii' },\n { label: 'Assembly', value: 'x86asm' },\n { label: 'Bash', value: 'bash' },\n { label: 'BASIC', value: 'basic' },\n { label: 'BNF', value: 'bnf' },\n { label: 'C', value: 'c' },\n { label: 'C#', value: 'csharp' },\n { label: 'C++', value: 'cpp' },\n { label: 'Clojure', value: 'clojure' },\n { label: 'CoffeeScript', value: 'coffeescript' },\n { label: 'Coq', value: 'coq' },\n { label: 'CSS', value: 'css' },\n { label: 'Dart', value: 'dart' },\n { label: 'Dhall', value: 'dhall' },\n { label: 'Diff', value: 'diff' },\n { label: 'Docker', value: 'dockerfile' },\n { label: 'EBNF', value: 'ebnf' },\n { label: 'Elixir', value: 'elixir' },\n { label: 'Elm', value: 'elm' },\n { label: 'Erlang', value: 'erlang' },\n { label: 'F#', value: 'fsharp' },\n { label: 'Flow', value: 'flow' },\n { label: 'Fortran', value: 'fortran' },\n { label: 'Gherkin', value: 'gherkin' },\n { label: 'GLSL', value: 'glsl' },\n { label: 'Go', value: 'go' },\n { label: 'GraphQL', value: 'graphql' },\n { label: 'Groovy', value: 'groovy' },\n { label: 'Haskell', value: 'haskell' },\n { label: 'HCL', value: 'hcl' },\n { label: 'HTML', value: 'html' },\n { label: 'Idris', value: 'idris' },\n { label: 'Java', value: 'java' },\n { label: 'JavaScript', value: 'javascript' },\n { label: 'JSON', value: 'json' },\n { label: 'Julia', value: 'julia' },\n { label: 'Kotlin', value: 'kotlin' },\n { label: 'LaTeX', value: 'latex' },\n { label: 'Less', value: 'less' },\n { label: 'Lisp', value: 'lisp' },\n { label: 'LiveScript', value: 'livescript' },\n { label: 'LLVM IR', value: 'llvm' },\n { label: 'Lua', value: 'lua' },\n { label: 'Makefile', value: 'makefile' },\n { label: 'Markdown', value: 'markdown' },\n { label: 'Markup', value: 'markup' },\n { label: 'MATLAB', value: 'matlab' },\n { label: 'Mathematica', value: 'mathematica' },\n { label: 'Mermaid', value: 'mermaid' },\n { label: 'Nix', value: 'nix' },\n { label: 'Notion Formula', value: 'notion' },\n { label: 'Objective-C', value: 'objectivec' },\n { label: 'OCaml', value: 'ocaml' },\n { label: 'Pascal', value: 'pascal' },\n { label: 'Perl', value: 'perl' },\n { label: 'PHP', value: 'php' },\n { label: 'PowerShell', value: 'powershell' },\n { label: 'Prolog', value: 'prolog' },\n { label: 'Protocol Buffers', value: 'protobuf' },\n { label: 'PureScript', value: 'purescript' },\n { label: 'Python', value: 'python' },\n { label: 'R', value: 'r' },\n { label: 'Racket', value: 'racket' },\n { label: 'Reason', value: 'reasonml' },\n { label: 'Ruby', value: 'ruby' },\n { label: 'Rust', value: 'rust' },\n { label: 'Sass', value: 'scss' },\n { label: 'Scala', value: 'scala' },\n { label: 'Scheme', value: 'scheme' },\n { label: 'SCSS', value: 'scss' },\n { label: 'Shell', value: 'shell' },\n { label: 'Smalltalk', value: 'smalltalk' },\n { label: 'Solidity', value: 'solidity' },\n { label: 'SQL', value: 'sql' },\n { label: 'Swift', value: 'swift' },\n { label: 'TOML', value: 'toml' },\n { label: 'TypeScript', value: 'typescript' },\n { label: 'VB.Net', value: 'vbnet' },\n { label: 'Verilog', value: 'verilog' },\n { label: 'VHDL', value: 'vhdl' },\n { label: 'Visual Basic', value: 'vbnet' },\n { label: 'WebAssembly', value: 'wasm' },\n { label: 'XML', value: 'xml' },\n { label: 'YAML', value: 'yaml' },\n];\n\nexport function CodeBlockCombobox() {\n const [open, setOpen] = useState(false);\n const readOnly = useReadOnly();\n const editor = useEditorRef();\n const element = useElement<TCodeBlockElement>();\n const value = element.lang || 'plaintext';\n const [searchValue, setSearchValue] = React.useState('');\n\n const items = React.useMemo(\n () =>\n languages.filter(\n (language) =>\n !searchValue ||\n language.label.toLowerCase().includes(searchValue.toLowerCase())\n ),\n [searchValue]\n );\n\n if (readOnly) return null;\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n size=\"xs\"\n variant=\"ghost\"\n className=\"h-6 justify-between gap-1 px-2 text-xs text-muted-foreground select-none\"\n aria-expanded={open}\n role=\"combobox\"\n >\n {languages.find((language) => language.value === value)?.label ??\n 'Plain Text'}\n </Button>\n </PopoverTrigger>\n <PopoverContent\n className=\"w-[200px] p-0\"\n onCloseAutoFocus={() => setSearchValue('')}\n >\n <Command shouldFilter={false}>\n <CommandInput\n className=\"h-9\"\n value={searchValue}\n onValueChange={(value) => setSearchValue(value)}\n placeholder=\"Search language...\"\n />\n <CommandEmpty>No language found.</CommandEmpty>\n\n <CommandList className=\"h-[344px] overflow-y-auto\">\n <CommandGroup>\n {items.map((language) => (\n <CommandItem\n key={language.label}\n className=\"cursor-pointer\"\n value={language.value}\n onSelect={(value) => {\n editor.tf.setNodes<TCodeBlockElement>(\n { lang: value },\n { at: element }\n );\n setSearchValue(value);\n setOpen(false);\n }}\n >\n <Check\n className={cn(\n value === language.value ? 'opacity-100' : 'opacity-0'\n )}\n />\n {language.label}\n </CommandItem>\n ))}\n </CommandGroup>\n </CommandList>\n </Command>\n </PopoverContent>\n </Popover>\n );\n}\n",
3636
"type": "registry:ui",
3737
"target": "components/plate-ui/code-block-combobox.tsx"
3838
}

apps/www/src/registry/default/plate-ui/code-block-combobox.tsx

+61-6
Original file line numberDiff line numberDiff line change
@@ -22,36 +22,91 @@ import { Popover, PopoverContent, PopoverTrigger } from './popover';
2222
const languages: { label: string; value: string }[] = [
2323
{ label: 'Auto', value: 'auto' },
2424
{ label: 'Plain Text', value: 'plaintext' },
25+
{ label: 'ABAP', value: 'abap' },
26+
{ label: 'Agda', value: 'agda' },
27+
{ label: 'Arduino', value: 'arduino' },
28+
{ label: 'ASCII Art', value: 'ascii' },
29+
{ label: 'Assembly', value: 'x86asm' },
2530
{ label: 'Bash', value: 'bash' },
31+
{ label: 'BASIC', value: 'basic' },
32+
{ label: 'BNF', value: 'bnf' },
2633
{ label: 'C', value: 'c' },
27-
{ label: 'C++', value: 'cpp' },
2834
{ label: 'C#', value: 'csharp' },
35+
{ label: 'C++', value: 'cpp' },
36+
{ label: 'Clojure', value: 'clojure' },
37+
{ label: 'CoffeeScript', value: 'coffeescript' },
38+
{ label: 'Coq', value: 'coq' },
2939
{ label: 'CSS', value: 'css' },
40+
{ label: 'Dart', value: 'dart' },
41+
{ label: 'Dhall', value: 'dhall' },
3042
{ label: 'Diff', value: 'diff' },
43+
{ label: 'Docker', value: 'dockerfile' },
44+
{ label: 'EBNF', value: 'ebnf' },
45+
{ label: 'Elixir', value: 'elixir' },
46+
{ label: 'Elm', value: 'elm' },
47+
{ label: 'Erlang', value: 'erlang' },
48+
{ label: 'F#', value: 'fsharp' },
49+
{ label: 'Flow', value: 'flow' },
50+
{ label: 'Fortran', value: 'fortran' },
51+
{ label: 'Gherkin', value: 'gherkin' },
52+
{ label: 'GLSL', value: 'glsl' },
3153
{ label: 'Go', value: 'go' },
3254
{ label: 'GraphQL', value: 'graphql' },
55+
{ label: 'Groovy', value: 'groovy' },
56+
{ label: 'Haskell', value: 'haskell' },
57+
{ label: 'HCL', value: 'hcl' },
3358
{ label: 'HTML', value: 'html' },
59+
{ label: 'Idris', value: 'idris' },
3460
{ label: 'Java', value: 'java' },
3561
{ label: 'JavaScript', value: 'javascript' },
3662
{ label: 'JSON', value: 'json' },
37-
{ label: 'JSX', value: 'jsx' },
63+
{ label: 'Julia', value: 'julia' },
3864
{ label: 'Kotlin', value: 'kotlin' },
65+
{ label: 'LaTeX', value: 'latex' },
3966
{ label: 'Less', value: 'less' },
67+
{ label: 'Lisp', value: 'lisp' },
68+
{ label: 'LiveScript', value: 'livescript' },
69+
{ label: 'LLVM IR', value: 'llvm' },
4070
{ label: 'Lua', value: 'lua' },
4171
{ label: 'Makefile', value: 'makefile' },
4272
{ label: 'Markdown', value: 'markdown' },
73+
{ label: 'Markup', value: 'markup' },
74+
{ label: 'MATLAB', value: 'matlab' },
75+
{ label: 'Mathematica', value: 'mathematica' },
76+
{ label: 'Mermaid', value: 'mermaid' },
77+
{ label: 'Nix', value: 'nix' },
78+
{ label: 'Notion Formula', value: 'notion' },
4379
{ label: 'Objective-C', value: 'objectivec' },
80+
{ label: 'OCaml', value: 'ocaml' },
81+
{ label: 'Pascal', value: 'pascal' },
82+
{ label: 'Perl', value: 'perl' },
4483
{ label: 'PHP', value: 'php' },
84+
{ label: 'PowerShell', value: 'powershell' },
85+
{ label: 'Prolog', value: 'prolog' },
86+
{ label: 'Protocol Buffers', value: 'protobuf' },
87+
{ label: 'PureScript', value: 'purescript' },
4588
{ label: 'Python', value: 'python' },
4689
{ label: 'R', value: 'r' },
90+
{ label: 'Racket', value: 'racket' },
91+
{ label: 'Reason', value: 'reasonml' },
4792
{ label: 'Ruby', value: 'ruby' },
4893
{ label: 'Rust', value: 'rust' },
94+
{ label: 'Sass', value: 'scss' },
95+
{ label: 'Scala', value: 'scala' },
96+
{ label: 'Scheme', value: 'scheme' },
4997
{ label: 'SCSS', value: 'scss' },
5098
{ label: 'Shell', value: 'shell' },
99+
{ label: 'Smalltalk', value: 'smalltalk' },
100+
{ label: 'Solidity', value: 'solidity' },
51101
{ label: 'SQL', value: 'sql' },
52102
{ label: 'Swift', value: 'swift' },
103+
{ label: 'TOML', value: 'toml' },
53104
{ label: 'TypeScript', value: 'typescript' },
54-
{ label: 'TSX', value: 'tsx' },
105+
{ label: 'VB.Net', value: 'vbnet' },
106+
{ label: 'Verilog', value: 'verilog' },
107+
{ label: 'VHDL', value: 'vhdl' },
108+
{ label: 'Visual Basic', value: 'vbnet' },
109+
{ label: 'WebAssembly', value: 'wasm' },
55110
{ label: 'XML', value: 'xml' },
56111
{ label: 'YAML', value: 'yaml' },
57112
];
@@ -61,7 +116,7 @@ export function CodeBlockCombobox() {
61116
const readOnly = useReadOnly();
62117
const editor = useEditorRef();
63118
const element = useElement<TCodeBlockElement>();
64-
const value = element.lang ?? 'plaintext';
119+
const value = element.lang || 'plaintext';
65120
const [searchValue, setSearchValue] = React.useState('');
66121

67122
const items = React.useMemo(
@@ -82,7 +137,7 @@ export function CodeBlockCombobox() {
82137
<Button
83138
size="xs"
84139
variant="ghost"
85-
className="h-6 justify-between text-muted-foreground gap-1 px-2 text-xs select-none"
140+
className="h-6 justify-between gap-1 px-2 text-xs text-muted-foreground select-none"
86141
aria-expanded={open}
87142
role="combobox"
88143
>
@@ -107,7 +162,7 @@ export function CodeBlockCombobox() {
107162
<CommandGroup>
108163
{items.map((language) => (
109164
<CommandItem
110-
key={language.value}
165+
key={language.label}
111166
className="cursor-pointer"
112167
value={language.value}
113168
onSelect={(value) => {

packages/code-block/src/lib/setCodeBlockToDecorations.spec.ts

-31
Original file line numberDiff line numberDiff line change
@@ -183,37 +183,6 @@ describe('codeBlockToDecorations', () => {
183183
expect(mockHighlightAuto).not.toHaveBeenCalled();
184184
});
185185

186-
it('should handle errors during highlighting', () => {
187-
// Mock highlight to throw an error
188-
mockHighlight.mockImplementation(() => {
189-
throw new Error('Highlighting error');
190-
});
191-
192-
// Create a code block
193-
const codeBlock: TCodeBlockElement = {
194-
children: [{ children: [{ text: 'const x = 1;' }], type: 'code_line' }],
195-
lang: 'javascript',
196-
type: 'code_block',
197-
};
198-
199-
const blockPath = [0];
200-
const result = codeBlockToDecorations(editor, [codeBlock, blockPath]);
201-
202-
// Should have one entry for the code line
203-
expect(result.size).toBe(1);
204-
205-
// The decorations for the line should be empty
206-
const lineDecorations = result.get(codeBlock.children[0] as any);
207-
expect(lineDecorations).toEqual([]);
208-
209-
// Error should be logged
210-
expect(editor.api.debug.error).toHaveBeenCalledWith(
211-
'Highlighting error:',
212-
'CODE_HIGHLIGHT',
213-
expect.any(Error)
214-
);
215-
});
216-
217186
it('should handle multiline code blocks', () => {
218187
// Mock highlight result for multiline code
219188
mockHighlight.mockReturnValue({

0 commit comments

Comments
 (0)