mirror of https://github.com/sveltejs/svelte
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
408 lines
9.4 KiB
408 lines
9.4 KiB
<script module>
|
|
export const cursorIndex = writable(0);
|
|
</script>
|
|
|
|
<script>
|
|
import { historyField } from '@codemirror/commands';
|
|
import { EditorState, Range, StateEffect, StateEffectType, StateField } from '@codemirror/state';
|
|
import { Decoration, EditorView } from '@codemirror/view';
|
|
import { codemirror, withCodemirrorInstance } from '@neocodemirror/svelte';
|
|
import { svelteLanguage } from '@replit/codemirror-lang-svelte';
|
|
import { javascriptLanguage } from '@codemirror/lang-javascript';
|
|
import { createEventDispatcher, tick } from 'svelte';
|
|
import { writable } from 'svelte/store';
|
|
import { get_repl_context } from '$lib/context.js';
|
|
import Message from './Message.svelte';
|
|
import { svelteTheme } from './theme.js';
|
|
import { autocomplete } from './autocomplete.js';
|
|
|
|
/** @type {import('@codemirror/lint').LintSource | undefined} */
|
|
export let diagnostics = undefined;
|
|
|
|
export let readonly = false;
|
|
export let tab = true;
|
|
|
|
/** @type {ReturnType<typeof createEventDispatcher<{ change: { value: string } }>>} */
|
|
const dispatch = createEventDispatcher();
|
|
|
|
let code = '';
|
|
|
|
/** @type {import('./types').Lang} */
|
|
let lang = 'svelte';
|
|
|
|
/**
|
|
* @param {{ code: string; lang: import('./types').Lang }} options
|
|
*/
|
|
export async function set(options) {
|
|
update(options);
|
|
}
|
|
|
|
/**
|
|
* @param {{ code?: string; lang?: import('./types').Lang }} options
|
|
*/
|
|
export async function update(options) {
|
|
await isReady;
|
|
|
|
if (!$cmInstance.view) return;
|
|
|
|
if (options.lang && options.lang !== lang) {
|
|
// This will trigger change_mode
|
|
lang = options.lang;
|
|
}
|
|
|
|
if (options.code !== undefined) {
|
|
updating_externally = true;
|
|
|
|
const { scrollLeft: left, scrollTop: top } = $cmInstance.view.scrollDOM;
|
|
|
|
code = options.code;
|
|
|
|
updating_externally = false;
|
|
|
|
$cmInstance.view.scrollDOM.scrollTop = top;
|
|
$cmInstance.view.scrollDOM.scrollLeft = left;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {number} pos
|
|
*/
|
|
export function setCursor(pos) {
|
|
cursor_pos = pos;
|
|
}
|
|
|
|
/** @type {(...val: any) => void} */
|
|
let fulfil_module_editor_ready;
|
|
export const isReady = new Promise((f) => (fulfil_module_editor_ready = f));
|
|
|
|
export function resize() {
|
|
$cmInstance.view?.requestMeasure();
|
|
}
|
|
|
|
export function focus() {
|
|
$cmInstance.view?.focus();
|
|
}
|
|
|
|
export function getEditorState() {
|
|
return $cmInstance.view?.state.toJSON({ history: historyField });
|
|
}
|
|
|
|
/**
|
|
* @param {any} state
|
|
*/
|
|
export function setEditorState(state) {
|
|
if (!$cmInstance.view) return;
|
|
|
|
$cmInstance.view.setState(
|
|
EditorState.fromJSON(state, { extensions, doc: state.doc }, { history: historyField })
|
|
);
|
|
$cmInstance.view?.dispatch({
|
|
changes: { from: 0, to: $cmInstance.view.state.doc.length, insert: state.doc },
|
|
effects: [StateEffect.reconfigure.of($cmInstance.extensions ?? [])]
|
|
});
|
|
}
|
|
|
|
export async function clearEditorState() {
|
|
await tick();
|
|
|
|
$cmInstance.view?.setState(EditorState.create({ extensions, doc: '' }));
|
|
$cmInstance.view?.dispatch({
|
|
changes: { from: 0, to: $cmInstance.view.state.doc.length, insert: '' },
|
|
effects: [StateEffect.reconfigure.of($cmInstance.extensions ?? [])]
|
|
});
|
|
}
|
|
|
|
/** @type {StateEffectType<Range<Decoration>[]>} */
|
|
const addMarksDecoration = StateEffect.define();
|
|
|
|
// This value must be added to the set of extensions to enable this
|
|
const markField = StateField.define({
|
|
// Start with an empty set of decorations
|
|
create() {
|
|
return Decoration.none;
|
|
},
|
|
// This is called whenever the editor updates—it computes the new set
|
|
update(value, tr) {
|
|
// Move the decorations to account for document changes
|
|
value = value.map(tr.changes);
|
|
// If this transaction adds or removes decorations, apply those changes
|
|
for (let effect of tr.effects) {
|
|
if (effect.is(addMarksDecoration)) value = value.update({ add: effect.value, sort: true });
|
|
}
|
|
return value;
|
|
},
|
|
// Indicate that this field provides a set of decorations
|
|
provide: (f) => EditorView.decorations.from(f)
|
|
});
|
|
|
|
/**
|
|
* @param {object} param0
|
|
* @param {number} param0.from
|
|
* @param {number} param0.to
|
|
* @param {string} [param0.className]
|
|
*/
|
|
export function markText({ from, to, className = 'mark-text' }) {
|
|
const executedMark = Decoration.mark({
|
|
class: className
|
|
});
|
|
|
|
$cmInstance.view?.dispatch({
|
|
effects: [
|
|
StateEffect.appendConfig.of(markField),
|
|
addMarksDecoration.of([executedMark.range(from, to)])
|
|
]
|
|
});
|
|
}
|
|
|
|
export function unmarkText() {
|
|
$cmInstance.view?.dispatch({
|
|
effects: StateEffect.reconfigure.of($cmInstance.extensions ?? [])
|
|
});
|
|
}
|
|
|
|
const cmInstance = withCodemirrorInstance();
|
|
|
|
/** @type {number} */
|
|
let w;
|
|
/** @type {number} */
|
|
let h;
|
|
|
|
let marked = false;
|
|
|
|
let updating_externally = false;
|
|
|
|
/** @type {import('@codemirror/state').Extension[]} */
|
|
let extensions = [];
|
|
|
|
let cursor_pos = 0;
|
|
|
|
$: if ($cmInstance.view) {
|
|
fulfil_module_editor_ready();
|
|
}
|
|
|
|
$: if ($cmInstance.view && w && h) resize();
|
|
|
|
$: if (marked) {
|
|
unmarkText();
|
|
marked = false;
|
|
}
|
|
|
|
const watcher = EditorView.updateListener.of((viewUpdate) => {
|
|
if (viewUpdate.selectionSet) {
|
|
cursorIndex.set(viewUpdate.state.selection.main.head);
|
|
}
|
|
});
|
|
|
|
const { files, selected } = get_repl_context();
|
|
|
|
const svelte_rune_completions = svelteLanguage.data.of({
|
|
/** @param {import('@codemirror/autocomplete').CompletionContext} context */
|
|
autocomplete: (context) => autocomplete(context, $selected, $files)
|
|
});
|
|
|
|
const js_rune_completions = javascriptLanguage.data.of({
|
|
/** @param {import('@codemirror/autocomplete').CompletionContext} context */
|
|
autocomplete: (context) => autocomplete(context, $selected, $files)
|
|
});
|
|
</script>
|
|
|
|
<div
|
|
class="codemirror-container"
|
|
use:codemirror={{
|
|
value: code,
|
|
setup: 'basic',
|
|
useTabs: tab,
|
|
tabSize: 2,
|
|
theme: svelteTheme,
|
|
readonly,
|
|
cursorPos: cursor_pos,
|
|
lang,
|
|
langMap: {
|
|
js: () => import('@codemirror/lang-javascript').then((m) => m.javascript()),
|
|
json: () => import('@codemirror/lang-json').then((m) => m.json()),
|
|
md: () => import('@codemirror/lang-markdown').then((m) => m.markdown()),
|
|
css: () => import('@codemirror/lang-css').then((m) => m.css()),
|
|
svelte: () => import('@replit/codemirror-lang-svelte').then((m) => m.svelte())
|
|
},
|
|
lint: diagnostics,
|
|
lintOptions: { delay: 200 },
|
|
autocomplete: true,
|
|
extensions: [svelte_rune_completions, js_rune_completions, watcher],
|
|
instanceStore: cmInstance
|
|
}}
|
|
on:codemirror:textChange={({ detail: value }) => {
|
|
code = value;
|
|
dispatch('change', { value: code });
|
|
}}
|
|
>
|
|
{#if !$cmInstance.view}
|
|
<pre style="position: absolute; left: 0; top: 0">{code}</pre>
|
|
|
|
<div style="position: absolute; width: 100%; bottom: 0">
|
|
<Message kind="info">loading editor...</Message>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<style>
|
|
.codemirror-container {
|
|
--warning: hsl(40 100% 70%);
|
|
--error: hsl(0 100% 90%);
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
border: none;
|
|
line-height: 1.5;
|
|
overflow: hidden;
|
|
}
|
|
|
|
:global(.dark) .codemirror-container {
|
|
--warning: hsl(40 100% 50%);
|
|
--error: hsl(0 100% 70%);
|
|
}
|
|
|
|
.codemirror-container :global {
|
|
* {
|
|
font: 400 var(--sk-text-xs) / 1.7 var(--sk-font-mono);
|
|
}
|
|
|
|
.mark-text {
|
|
background-color: var(--sk-selection-color);
|
|
backdrop-filter: opacity(40%);
|
|
}
|
|
|
|
.cm-editor {
|
|
height: 100%;
|
|
}
|
|
|
|
.error-loc {
|
|
position: relative;
|
|
border-bottom: 2px solid #da106e;
|
|
}
|
|
|
|
.error-line {
|
|
background-color: rgba(200, 0, 0, 0.05);
|
|
}
|
|
|
|
.cm-tooltip {
|
|
border: none;
|
|
background: var(--sk-back-3);
|
|
font-family: var(--sk-font);
|
|
max-width: calc(100vw - 10em);
|
|
position: relative;
|
|
filter: drop-shadow(2px 4px 6px rgba(0, 0, 0, 0.1));
|
|
}
|
|
|
|
.cm-tooltip-section {
|
|
position: relative;
|
|
padding: 0.5em;
|
|
left: -13px;
|
|
background: var(--bg);
|
|
border-radius: 2px;
|
|
max-width: 64em;
|
|
}
|
|
|
|
.cm-tooltip-section::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 10px;
|
|
width: 8px;
|
|
height: 8px;
|
|
transform: rotate(45deg);
|
|
background-color: var(--bg);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.cm-tooltip-below .cm-tooltip-section {
|
|
top: 10px;
|
|
}
|
|
|
|
.cm-tooltip-above .cm-tooltip-section {
|
|
bottom: 10px;
|
|
}
|
|
|
|
.cm-tooltip-below .cm-tooltip-section::before {
|
|
top: -4px;
|
|
}
|
|
|
|
.cm-tooltip-above .cm-tooltip-section::before {
|
|
bottom: -4px;
|
|
}
|
|
|
|
.cm-tooltip:has(.cm-diagnostic) {
|
|
background: transparent;
|
|
}
|
|
|
|
.cm-tooltip:has(.cm-diagnostic-warning) {
|
|
--bg: var(--warning);
|
|
--fg: #222;
|
|
}
|
|
|
|
.cm-tooltip:has(.cm-diagnostic-error) {
|
|
--bg: var(--error);
|
|
--fg: #222;
|
|
}
|
|
|
|
.cm-diagnostic {
|
|
padding: 0.2em 0.4em;
|
|
position: relative;
|
|
border: none;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.cm-diagnostic:not(:last-child) {
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.cm-diagnostic-error {
|
|
border: none;
|
|
filter: drop-shadow(0px 0px 6px var(--error-bg));
|
|
}
|
|
|
|
.cm-diagnostic :not(code) {
|
|
font-family: var(--sk-font);
|
|
}
|
|
|
|
.cm-diagnosticText {
|
|
color: var(--fg);
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
|
|
.cm-diagnosticText code {
|
|
color: inherit;
|
|
background-color: rgba(0, 0, 0, 0.05);
|
|
border-radius: 2px;
|
|
top: 0;
|
|
padding: 0.2em;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.cm-diagnosticText strong {
|
|
font-size: 0.9em;
|
|
/* font-weight: 700; */
|
|
font-family: var(--sk-font-mono);
|
|
opacity: 0.7;
|
|
}
|
|
}
|
|
|
|
pre {
|
|
position: absolute;
|
|
width: 100%;
|
|
height: 100%;
|
|
top: 0;
|
|
left: 0;
|
|
border: none;
|
|
padding: 4px 4px 4px 60px;
|
|
resize: none;
|
|
font-family: var(--sk-font-mono);
|
|
font-size: 13px;
|
|
line-height: 1.7;
|
|
user-select: none;
|
|
pointer-events: none;
|
|
color: #ccc;
|
|
tab-size: 2;
|
|
-moz-tab-size: 2;
|
|
}
|
|
</style>
|