Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix codeblock #4132

Merged
merged 2 commits into from
Mar 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/blue-dancers-dream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@udecode/plate-selection': patch
---

Fix multi-block deletion, cmd+a while selecting, arrow up/down while selecting all
5 changes: 5 additions & 0 deletions .changeset/funny-ducks-care.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@udecode/plate-code-block': patch
---

Support invalid languages
5 changes: 5 additions & 0 deletions .changeset/honest-colts-fly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@udecode/plate-core': patch
---

Better error message for editor.api.debug.error
2 changes: 1 addition & 1 deletion apps/www/content/docs/en/components/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Plate 46 - new code block
- `code-syntax-leaf-static`, `code-syntax-leaf`: Updated to use lowlight token classes
- Removed `prismjs` dependency and related styles
- Use `lowlight` plugin option instead of `prism` option
- `code-block-combobox`: add `Auto` language option
- `code-block-combobox`: add `Auto` language option, change language values to match lowlight
- `autoformat-plugin`: prevent autoformat on code blocks

```tsx
Expand Down
2 changes: 1 addition & 1 deletion apps/www/public/r/styles/default/code-block-element.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
},
{
"path": "plate-ui/code-block-combobox.tsx",
"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",
"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",
"type": "registry:ui",
"target": "components/plate-ui/code-block-combobox.tsx"
}
Expand Down
67 changes: 61 additions & 6 deletions apps/www/src/registry/default/plate-ui/code-block-combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,36 +22,91 @@ import { Popover, PopoverContent, PopoverTrigger } from './popover';
const languages: { label: string; value: string }[] = [
{ label: 'Auto', value: 'auto' },
{ label: 'Plain Text', value: 'plaintext' },
{ label: 'ABAP', value: 'abap' },
{ label: 'Agda', value: 'agda' },
{ label: 'Arduino', value: 'arduino' },
{ label: 'ASCII Art', value: 'ascii' },
{ label: 'Assembly', value: 'x86asm' },
{ label: 'Bash', value: 'bash' },
{ label: 'BASIC', value: 'basic' },
{ label: 'BNF', value: 'bnf' },
{ label: 'C', value: 'c' },
{ label: 'C++', value: 'cpp' },
{ label: 'C#', value: 'csharp' },
{ label: 'C++', value: 'cpp' },
{ label: 'Clojure', value: 'clojure' },
{ label: 'CoffeeScript', value: 'coffeescript' },
{ label: 'Coq', value: 'coq' },
{ label: 'CSS', value: 'css' },
{ label: 'Dart', value: 'dart' },
{ label: 'Dhall', value: 'dhall' },
{ label: 'Diff', value: 'diff' },
{ label: 'Docker', value: 'dockerfile' },
{ label: 'EBNF', value: 'ebnf' },
{ label: 'Elixir', value: 'elixir' },
{ label: 'Elm', value: 'elm' },
{ label: 'Erlang', value: 'erlang' },
{ label: 'F#', value: 'fsharp' },
{ label: 'Flow', value: 'flow' },
{ label: 'Fortran', value: 'fortran' },
{ label: 'Gherkin', value: 'gherkin' },
{ label: 'GLSL', value: 'glsl' },
{ label: 'Go', value: 'go' },
{ label: 'GraphQL', value: 'graphql' },
{ label: 'Groovy', value: 'groovy' },
{ label: 'Haskell', value: 'haskell' },
{ label: 'HCL', value: 'hcl' },
{ label: 'HTML', value: 'html' },
{ label: 'Idris', value: 'idris' },
{ label: 'Java', value: 'java' },
{ label: 'JavaScript', value: 'javascript' },
{ label: 'JSON', value: 'json' },
{ label: 'JSX', value: 'jsx' },
{ label: 'Julia', value: 'julia' },
{ label: 'Kotlin', value: 'kotlin' },
{ label: 'LaTeX', value: 'latex' },
{ label: 'Less', value: 'less' },
{ label: 'Lisp', value: 'lisp' },
{ label: 'LiveScript', value: 'livescript' },
{ label: 'LLVM IR', value: 'llvm' },
{ label: 'Lua', value: 'lua' },
{ label: 'Makefile', value: 'makefile' },
{ label: 'Markdown', value: 'markdown' },
{ label: 'Markup', value: 'markup' },
{ label: 'MATLAB', value: 'matlab' },
{ label: 'Mathematica', value: 'mathematica' },
{ label: 'Mermaid', value: 'mermaid' },
{ label: 'Nix', value: 'nix' },
{ label: 'Notion Formula', value: 'notion' },
{ label: 'Objective-C', value: 'objectivec' },
{ label: 'OCaml', value: 'ocaml' },
{ label: 'Pascal', value: 'pascal' },
{ label: 'Perl', value: 'perl' },
{ label: 'PHP', value: 'php' },
{ label: 'PowerShell', value: 'powershell' },
{ label: 'Prolog', value: 'prolog' },
{ label: 'Protocol Buffers', value: 'protobuf' },
{ label: 'PureScript', value: 'purescript' },
{ label: 'Python', value: 'python' },
{ label: 'R', value: 'r' },
{ label: 'Racket', value: 'racket' },
{ label: 'Reason', value: 'reasonml' },
{ label: 'Ruby', value: 'ruby' },
{ label: 'Rust', value: 'rust' },
{ label: 'Sass', value: 'scss' },
{ label: 'Scala', value: 'scala' },
{ label: 'Scheme', value: 'scheme' },
{ label: 'SCSS', value: 'scss' },
{ label: 'Shell', value: 'shell' },
{ label: 'Smalltalk', value: 'smalltalk' },
{ label: 'Solidity', value: 'solidity' },
{ label: 'SQL', value: 'sql' },
{ label: 'Swift', value: 'swift' },
{ label: 'TOML', value: 'toml' },
{ label: 'TypeScript', value: 'typescript' },
{ label: 'TSX', value: 'tsx' },
{ label: 'VB.Net', value: 'vbnet' },
{ label: 'Verilog', value: 'verilog' },
{ label: 'VHDL', value: 'vhdl' },
{ label: 'Visual Basic', value: 'vbnet' },
{ label: 'WebAssembly', value: 'wasm' },
{ label: 'XML', value: 'xml' },
{ label: 'YAML', value: 'yaml' },
];
Expand All @@ -61,7 +116,7 @@ export function CodeBlockCombobox() {
const readOnly = useReadOnly();
const editor = useEditorRef();
const element = useElement<TCodeBlockElement>();
const value = element.lang ?? 'plaintext';
const value = element.lang || 'plaintext';
const [searchValue, setSearchValue] = React.useState('');

const items = React.useMemo(
Expand All @@ -82,7 +137,7 @@ export function CodeBlockCombobox() {
<Button
size="xs"
variant="ghost"
className="h-6 justify-between text-muted-foreground gap-1 px-2 text-xs select-none"
className="h-6 justify-between gap-1 px-2 text-xs text-muted-foreground select-none"
aria-expanded={open}
role="combobox"
>
Expand All @@ -107,7 +162,7 @@ export function CodeBlockCombobox() {
<CommandGroup>
{items.map((language) => (
<CommandItem
key={language.value}
key={language.label}
className="cursor-pointer"
value={language.value}
onSelect={(value) => {
Expand Down
31 changes: 0 additions & 31 deletions packages/code-block/src/lib/setCodeBlockToDecorations.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,37 +183,6 @@ describe('codeBlockToDecorations', () => {
expect(mockHighlightAuto).not.toHaveBeenCalled();
});

it('should handle errors during highlighting', () => {
// Mock highlight to throw an error
mockHighlight.mockImplementation(() => {
throw new Error('Highlighting error');
});

// Create a code block
const codeBlock: TCodeBlockElement = {
children: [{ children: [{ text: 'const x = 1;' }], type: 'code_line' }],
lang: 'javascript',
type: 'code_block',
};

const blockPath = [0];
const result = codeBlockToDecorations(editor, [codeBlock, blockPath]);

// Should have one entry for the code line
expect(result.size).toBe(1);

// The decorations for the line should be empty
const lineDecorations = result.get(codeBlock.children[0] as any);
expect(lineDecorations).toEqual([]);

// Error should be logged
expect(editor.api.debug.error).toHaveBeenCalledWith(
'Highlighting error:',
'CODE_HIGHLIGHT',
expect.any(Error)
);
});

it('should handle multiline code blocks', () => {
// Mock highlight result for multiline code
mockHighlight.mockReturnValue({
Expand Down
Loading