Last active
November 1, 2022 20:17
-
-
Save ppcamp/5fb7fcd9951c6072acda3ee946577964 to your computer and use it in GitHub Desktop.
CodeMirror v6 simple implementation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- | |
@component | |
# CodeMirror | |
A simple CodeMirror implementation that can be used as base for the text editor. | |
## Args | |
- **value**: `string` The textarea field | |
- **initialText**: `string` The textarea initial value | |
- **hasFocus**: `boolean` Property got from CodeMirror editor | |
- **showNumbers**: `boolean` Enables the column of numbers on the left | |
- **fontFamily**: `string` Change the detaulf CodeMirror editor font family | |
- **placeHolder**: `string` Change the detaulf text placeholder | |
- **matchPattern**: `RegExp` Pattern that will be used to search on the current word/string | |
- **completions**: `Array<Completion>` Array of strings that will be used to suggest basing on | |
fuzzy logic. Our list of completions (can be static, since the editor will do filtering based | |
on context). | |
## Events | |
- **change**: `CustomEvent<string>` the current value | |
- **select**: `CustomEvent<ChangeSelection>` the current selection | |
- **geometryChanged**: `undefined` when change the current object selection | |
## Usefull Tips / Keymaps | |
[Default Keymaps](https://codemirror.net/docs/ref/#h_keymaps) (NOTE: sometimes the system/browser change them) | |
- `Ctrl+Space` opens the popup autocomplete suggestion | |
- `Ctrl+f` shows the search menu that supports replacing and regex options | |
- `Ctrl+g` **custom** move the selector to a given line | |
- `Alt+MouseLeftHold selection` adds cursors to lines | |
- `Ctrl+d` add cursor to the next matching pattern | |
- `Ctrl+a` selects all | |
- `Alt+ ArrowUp/Down` moves the current line to up or down | |
- `Esc` clear selection | |
- `Ctrl+Enter` add a new line | |
- `Alt+l` select current line | |
- `Home/End` moves to start/end line | |
- `Ctrl+Home/End` moves to start/end textarea | |
## Example | |
<script lang="ts"> | |
import type { Completion } from "@codemirror/autocomplete"; | |
import CodeMirror, { type ChangeSelection } from "./CodeMirror.svelte"; | |
const placeHolder = "Type string to see the completions"; | |
const completions: Array<Completion> = [ | |
{ label: "string.size()", type: "string" }, | |
{ label: "string.forEach(v=>fn(v))", type: "string" }, | |
{ label: "string.find(v=>", type: "string" }, | |
]; | |
const onChange = (event: CustomEvent<string>) => { | |
console.log("Change", event.detail); | |
}; | |
const onSelect = (event: CustomEvent<ChangeSelection>) => { | |
console.log("Select", event.detail); | |
}; | |
const onGeometryChanged = () => { | |
console.log("Change geometry"); | |
}; | |
const onFocus = (event: CustomEvent<any>) => { | |
console.log("Focus", event.detail); | |
}; | |
</script> | |
<CodeMirror | |
{completions} | |
showNumbers | |
{placeHolder} | |
on:change={onChange} | |
on:select={onSelect} | |
on:geometryChanged={onGeometryChanged} | |
/> | |
## See | |
- https://stackoverflow.com/a/72407564 | |
- https://codemirror.net/docs/ref/#autocomplete.Completion | |
- https://codemirror.net/examples/styling/ | |
- https://discuss.codemirror.net/t/changing-the-font-size-of-cm6/2935/6 | |
## TODO | |
- Change the cursor position for FUNCTIONS types in each autocomplete. | |
Example: | |
// $1 means cursor | |
s.func(v=>fn(v))$1 -> s.func(v=>fn) | |
## Needed packages | |
`npm i codemirror @codemirror` | |
--> | |
<script lang="ts" context="module"> | |
export type ChangeSelection = Array<{ from: number; to: number }>; | |
</script> | |
<script lang="ts"> | |
import { basicSetup, EditorView } from "codemirror"; | |
import { | |
autocompletion, | |
CompletionContext, | |
type Completion, | |
type CompletionSource, | |
} from "@codemirror/autocomplete"; | |
import { placeholder, keymap } from "@codemirror/view"; | |
import { EditorState } from "@codemirror/state"; | |
import { insertTab } from "@codemirror/commands"; | |
import { createEventDispatcher, onMount } from "svelte"; | |
export let value: string = ""; | |
export let matchPattern: RegExp = /[\w._]+/; | |
export let completions: Array<Completion> = []; | |
export let hasFocus: boolean = false; | |
export let showNumbers: boolean = false; | |
export let fontFamily: string = "'JetBrains Mono', monospace"; | |
export let placeHolder: string = "Type some text"; | |
let component: Element | undefined; | |
let editor: EditorView | undefined; | |
$: hasFocus = editor?.hasFocus || false; | |
const dispatch = createEventDispatcher<{ | |
change: string; | |
select: ChangeSelection; | |
geometryChanged?: undefined; | |
}>(); | |
const cssTheme = EditorView.theme( | |
{ | |
"&": { | |
color: "grey", | |
backgroundColor: "#fefefe", | |
fontSize: "10pt", | |
border: "1px solid #aaa", | |
borderRadius: "5px", | |
height: "100px", | |
overflow: "auto", | |
}, | |
".cm-content": { | |
fontFamily, | |
caretColor: "black", | |
}, | |
"&.cm-editor.cm-focused": { | |
outline: "2px solid var(--main-color)", | |
}, | |
".cm-gutters": { | |
display: !showNumbers ? "none" : "flex", | |
}, | |
"&.cm-focused .cm-selectionBackground, ::selection": { | |
backgroundColor: "var(--main-color)", | |
color: "white", | |
}, | |
".cm-activeLine.cm-line": { | |
backgroundColor: "rgba(0,0,0,0)", | |
}, | |
".cm-activeLineGutter": { | |
backgroundColor: "var(--main-color)", | |
color: "white", | |
}, | |
".cm-placeholder": { | |
fontStyle: "italic", | |
}, | |
}, | |
{ dark: false } | |
); | |
// command | |
const autocomplete: CompletionSource = (context: CompletionContext) => { | |
let before = context.matchBefore(matchPattern); | |
// If completion wasn't explicitly started and there | |
// is no word before the cursor, don't open completions. | |
if (!context.explicit && !before) return null; | |
return { | |
from: before ? before.from : context.pos, | |
options: completions, | |
validFor: /^\w*$/, | |
}; | |
}; | |
// command | |
const moveToLine = (view: EditorView) => { | |
const line = prompt("Which line?") ?? ""; | |
if (!/^\d+$/.test(line) || +line <= 0 || +line > view.state.doc.lines) | |
return false; | |
const pos = view.state.doc.line(+line).from; | |
view.dispatch({ selection: { anchor: pos }, userEvent: "select" }); | |
return true; | |
}; | |
onMount(() => { | |
editor = new EditorView({ | |
state: EditorState.create({ | |
doc: value, | |
extensions: [ | |
keymap.of([ | |
{ key: "Tab", run: insertTab }, | |
{ key: "Ctrl-g", run: moveToLine }, | |
]), | |
basicSetup, | |
cssTheme, | |
placeholder(placeHolder), | |
autocompletion({ override: [autocomplete] }), | |
EditorView.updateListener.of(function (e) { | |
value = e.state.doc.toString(); | |
if (e.docChanged) dispatch("change", value); | |
else if (e.selectionSet) { | |
// @ts-ignore they have the same fields | |
dispatch("select", e.state.selection.ranges); | |
} else if (e.geometryChanged) { | |
dispatch("geometryChanged"); | |
} | |
}), | |
], | |
}), | |
parent: component, | |
}); | |
}); | |
</script> | |
<div> | |
<span /> | |
<div bind:this={component} /> | |
</div> | |
<style lang="css"> | |
* { | |
--main-color: var(--mdc-theme-primary, #1a73e8); | |
} | |
</style> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment