Skip to content

Instantly share code, notes, and snippets.

@ppcamp
Last active November 1, 2022 20:17
Show Gist options
  • Save ppcamp/5fb7fcd9951c6072acda3ee946577964 to your computer and use it in GitHub Desktop.
Save ppcamp/5fb7fcd9951c6072acda3ee946577964 to your computer and use it in GitHub Desktop.
CodeMirror v6 simple implementation
<!--
@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