diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 0000000..58d9cf7
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,33 @@
+{
+ "name": "Solid Monaco Development",
+ "image": "node:24-bullseye",
+ "features": {
+ "ghcr.io/devcontainers/features/git:1": {}
+ },
+ "customizations": {
+ "vscode": {
+ "extensions": [
+ "esbenp.prettier-vscode",
+ "ms-vscode.vscode-typescript-next"
+ ],
+ "settings": {
+ "editor.formatOnSave": true,
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
+ "typescript.preferences.importModuleSpecifier": "relative"
+ }
+ }
+ },
+ "postCreateCommand": "corepack enable pnpm && pnpm install",
+ "remoteUser": "root",
+ "workspaceFolder": "/workspace",
+ "mounts": [
+ "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached"
+ ],
+ "forwardPorts": [5173],
+ "portsAttributes": {
+ "5173": {
+ "label": "Vite Dev Server",
+ "onAutoForward": "notify"
+ }
+ }
+}
\ No newline at end of file
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 3f37208..53f6635 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -12,35 +12,50 @@ jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
+ timeout-minutes: 360
permissions:
security-events: write
+ packages: read
+ actions: read
+ contents: read
+
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - language: javascript-typescript
+ build-mode: none
steps:
- name: Checkout repository
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Initialize CodeQL
- uses: github/codeql-action/init@v1
+ uses: github/codeql-action/init@v3
with:
- languages: javascript
+ languages: ${{ matrix.language }}
+ build-mode: ${{ matrix.build-mode }}
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v1
+ uses: github/codeql-action/analyze@v3
with:
+ category: "/language:${{ matrix.language }}"
upload: false
output: sarif-results
# Only include files that are public
- name: filter-sarif
- uses: advanced-security/filter-sarif@main
+ uses: advanced-security/filter-sarif@v1
with:
patterns: |
- /src/**/*.*
+ +src/**/*.ts
+ +src/**/*.tsx
-**/*.test.*
- input: sarif-results/javascript.sarif
- output: sarif-results/javascript.sarif
+ -**/*.spec.*
+ input: sarif-results/${{ matrix.language }}.sarif
+ output: sarif-results/${{ matrix.language }}.sarif
- name: Upload SARIF
- uses: github/codeql-action/upload-sarif@v1
+ uses: github/codeql-action/upload-sarif@v3
with:
- sarif_file: sarif-results/javascript.sarif
+ sarif_file: sarif-results/${{ matrix.language }}.sarif
diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml
index 4280668..37de4be 100644
--- a/.github/workflows/format.yml
+++ b/.github/workflows/format.yml
@@ -14,13 +14,16 @@ jobs:
contents: write
steps:
- - uses: actions/checkout@v3
- - uses: pnpm/action-setup@v2.2.4
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
+ with:
+ version: 8
- name: Setup Node.js environment
- uses: actions/setup-node@v3
+ uses: actions/setup-node@v4
with:
- node-version: 18
+ node-version: 20
+ cache: pnpm
# "git restore ." discards changes to package-lock.json
- name: Install dependencies
@@ -32,6 +35,6 @@ jobs:
run: pnpm run format
- name: Add, Commit and Push
- uses: stefanzweifel/git-auto-commit-action@v4
+ uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: 'Format'
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..e4cd7e1
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,55 @@
+name: Release
+
+on:
+ push:
+ tags:
+ - 'v*'
+
+jobs:
+ release:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ id-token: write
+
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 8
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: pnpm
+ registry-url: 'https://registry.npmjs.org'
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Run tests
+ run: pnpm run test
+
+ - name: Build package
+ run: pnpm run build
+
+ - name: Publish to NPM
+ run: pnpm publish --no-git-checks
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+
+ - name: Create GitHub Release
+ uses: actions/create-release@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ tag_name: ${{ github.ref }}
+ release_name: Release ${{ github.ref }}
+ draft: false
+ prerelease: false
\ No newline at end of file
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index f31b0a3..2e741f9 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -7,25 +7,39 @@ on:
branches: [main]
jobs:
- build:
+ test:
+ name: Test (Node ${{ matrix.node-version }})
runs-on: ubuntu-latest
-
+
+ strategy:
+ matrix:
+ node-version: [18, 20, 22]
+
steps:
- name: Checkout repo
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
fetch-depth: 2
- - uses: pnpm/action-setup@v2.2.4
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 8
- - name: Setup Node.js environment
- uses: actions/setup-node@v3
+ - name: Setup Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v4
with:
- node-version: 18
+ node-version: ${{ matrix.node-version }}
cache: pnpm
- name: Install dependencies
- run: pnpm install
+ run: pnpm install --frozen-lockfile
+
+ - name: Type check
+ run: pnpm run lint:types
+
+ - name: Lint
+ run: pnpm run lint:code
- name: Build
run: pnpm run build
@@ -35,5 +49,52 @@ jobs:
env:
CI: true
- - name: Lint
- run: pnpm run lint
+ build-check:
+ name: Build Check
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 8
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: pnpm
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Build library
+ run: pnpm run build
+
+ - name: Check build output
+ run: |
+ if [ ! -d "dist" ]; then
+ echo "❌ Build failed: dist directory not found"
+ exit 1
+ fi
+ if [ ! -f "dist/index.js" ]; then
+ echo "❌ Build failed: index.js not found"
+ exit 1
+ fi
+ if [ ! -f "dist/index.d.ts" ]; then
+ echo "❌ Build failed: index.d.ts not found"
+ exit 1
+ fi
+ echo "✅ Build output verified"
+
+ - name: Test package installation
+ run: |
+ # Create a test project to verify the package can be installed
+ mkdir test-install
+ cd test-install
+ npm init -y
+ npm install ../
+ echo "✅ Package installation test passed"
diff --git a/.gitignore b/.gitignore
index fb13f66..5a1d742 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@ dist
gitignore
.idea
.vscode
+.pnpm-store
# tsup
tsup.config.bundled_*.{m,c,}s
diff --git a/README.md b/README.md
index 39f3522..3e15896 100644
--- a/README.md
+++ b/README.md
@@ -15,12 +15,15 @@ Install it:
```bash
npm i solid-monaco
```
-*or*
+
+_or_
```bash
yarn add solid-monaco
```
-*or*
+
+_or_
+
```bash
pnpm add solid-monaco
```
@@ -44,9 +47,10 @@ function MyEditor() {
The `MonacoEditor` component accepts the following props:
| Prop | Type | Default | Description |
-|--------------------|--------------------------------------------------------------------|--------------|--------------------------------------------------------------------------------|
+| ------------------ | ------------------------------------------------------------------ | ------------ | ------------------------------------------------------------------------------ |
| `language` | `string` | - | The programming language for the editor. E.g., `"javascript"`, `"typescript"`. |
| `value` | `string` | - | Content of the editor. |
+| `line` | `number` | - | Jump to specific line number in the editor. |
| `loadingState` | `JSX.Element` | `"Loading…"` | JSX element to be displayed during the loading state. |
| `class` | `string` | - | CSS class for the editor container. |
| `theme` | `BuiltinTheme` or `string` | `"vs"` | The theme to be applied to the editor. |
@@ -57,8 +61,10 @@ The `MonacoEditor` component accepts the following props:
| `options` | `object` | - | Additional options for the Monaco editor. |
| `saveViewState` | `string` | `true` | Whether to save the model view state for a given path of the editor. |
| `onChange` | `(value: string, event: editor.IModelContentChangedEvent) => void` | - | Callback triggered when the content of the editor changes. |
+| `onBeforeMount` | `(monaco: Monaco) => void` | - | Callback triggered before editor creation for setup. |
| `onMount` | `(monaco: Monaco, editor: editor.IStandaloneCodeEditor) => void` | - | Callback triggered when the editor mounts. |
| `onBeforeUnmount` | `(monaco: Monaco, editor: editor.IStandaloneCodeEditor) => void` | - | Callback triggered before the editor unmounts. |
+| `onValidate` | `(markers: editor.IMarker[]) => void` | - | Callback triggered when validation markers change. |
### Getting Monaco and Editor Instances
@@ -72,16 +78,102 @@ function MyEditor() {
// Use monaco and editor instances here
};
+ return (
+
+ );
+}
+```
+
+#### Line Positioning
+
+Jump to a specific line number in the editor:
+
+```jsx
+import { MonacoEditor } from 'solid-monaco';
+import { createSignal } from 'solid-js';
+
+function MyEditor() {
+ const [currentLine, setCurrentLine] = createSignal(42);
+
+ return (
+
+
+
+
+ );
+}
+```
+
+#### Pre-Editor Setup
+
+Use the `beforeMount` callback to configure Monaco before the editor is created:
+
+```jsx
+import { MonacoEditor } from 'solid-monaco';
+
+function MyEditor() {
+ const handleBeforeMount = monaco => {
+ // Configure Monaco before editor creation
+ monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
+ noSemanticValidation: true,
+ noSyntaxValidation: false,
+ });
+
+ // Register custom themes, languages, etc.
+ monaco.editor.defineTheme('myCustomTheme', {
+ base: 'vs-dark',
+ inherit: true,
+ rules: [],
+ colors: {
+ 'editor.background': '#1e1e1e',
+ },
+ });
+ };
+
return (
);
}
```
+#### Validation Markers
+
+Monitor validation errors and warnings in real-time:
+
+```jsx
+import { MonacoEditor } from 'solid-monaco';
+import { createSignal } from 'solid-js';
+
+function MyEditor() {
+ const [errors, setErrors] = createSignal([]);
+
+ const handleValidate = markers => {
+ setErrors(markers.filter(marker => marker.severity === 8)); // Errors only
+ console.log('Validation markers:', markers);
+ };
+
+ return (
+
+
Errors: {errors().length}
+
+
+ );
+}
+```
+
## MonacoDiffEditor
For a side-by-side comparison view of code, the package provides a `MonacoDiffEditor` component.
@@ -109,25 +201,214 @@ function MyDiffEditor() {
The `MonacoDiffEditor` component accepts the following props:
-| Prop | Type | Default | Description |
-|--------------------|------------------------------------------------------------------|--------------|------------------------------------------------------------------------|
-| `original` | `string` | - | Original content to be displayed on the left side of the diff editor. |
-| `modified` | `string` | - | Modified content to be displayed on the right side of the diff editor. |
-| `originalLanguage` | `string` | - | Language for the original content. |
-| `modifiedLanguage` | `string` | - | Language for the modified content. |
-| `originalPath` | `string` | - | Path for the original content used in Monaco model management. |
-| `modifiedPath` | `string` | - | Path for the modified content used in Monaco model management. |
-| `loadingState` | `JSX.Element` | `"Loading…"` | JSX element displayed during the loading state. |
-| `class` | `string` | - | CSS class for the diff editor container. |
-| `theme` | `BuiltinTheme` or `string` | `"vs"` | Theme applied to the diff editor. |
-| `overrideServices` | `object` | - | Services to override the default ones provided by Monaco. |
-| `width` | `string` | `"100%"` | Width of the diff editor container. |
-| `height` | `string` | `"100%"` | Height of the diff editor container. |
-| `options` | `object` | - | Additional options for the Monaco diff editor. |
-| `saveViewState` | `boolean` | `true` | Whether to save the model view state. |
-| `onChange` | `(value: string) => void` | - | Callback triggered when the content of the modified editor changes. |
-| `onMount` | `(monaco: Monaco, editor: editor.IStandaloneDiffEditor) => void` | - | Callback triggered when the diff editor mounts. |
-| `onBeforeUnmount` | `(monaco: Monaco, editor: editor.IStandaloneDiffEditor) => void` | - | Callback triggered before the diff editor unmounts. |
+| Prop | Type | Default | Description |
+| ------------------ | ---------------------------------------------------------------- | ------------ | -------------------------------------------------------------------------------- |
+| `original` | `string` | - | Original content to be displayed on the left side of the diff editor. |
+| `modified` | `string` | - | Modified content to be displayed on the right side of the diff editor. |
+| `originalLanguage` | `string` | - | Language for the original content. |
+| `modifiedLanguage` | `string` | - | Language for the modified content. |
+| `originalPath` | `string` | - | Path for the original content used in Monaco model management. |
+| `modifiedPath` | `string` | - | Path for the modified content used in Monaco model management. |
+| `loadingState` | `JSX.Element` | `"Loading…"` | JSX element displayed during the loading state. |
+| `class` | `string` | - | CSS class for the diff editor container. |
+| `theme` | `BuiltinTheme` or `string` | `"vs"` | Theme applied to the diff editor. |
+| `overrideServices` | `object` | - | Services to override the default ones provided by Monaco. |
+| `width` | `string` | `"100%"` | Width of the diff editor container. |
+| `height` | `string` | `"100%"` | Height of the diff editor container. |
+| `options` | `IStandaloneDiffEditorConstructionOptions` | - | Correct diff editor options type (was incorrectly using regular editor options). |
+| `saveViewState` | `boolean` | `true` | Whether to save the model view state. |
+| `onChange` | `(value: string) => void` | - | Callback triggered when the content of the modified editor changes. |
+| `onMount` | `(monaco: Monaco, editor: editor.IStandaloneDiffEditor) => void` | - | Callback triggered when the diff editor mounts. |
+| `onBeforeUnmount` | `(monaco: Monaco, editor: editor.IStandaloneDiffEditor) => void` | - | Callback triggered before the diff editor unmounts. |
+| `onBeforeMount` | `(monaco: Monaco) => void` | - | Callback triggered before diff editor creation for setup. |
+
+## Production Setup Guide
+
+### Asset Loading Configuration
+
+The library supports both CDN and local asset loading strategies.
+
+#### CDN Loading (Default)
+
+```jsx
+// Uses CDN by default - no configuration needed
+
+```
+
+#### Local Asset Loading
+
+**1. Install Dependencies**
+
+Ensure `monaco-editor` is a regular dependency (not devDependency) in your `package.json`:
+
+```json
+{
+ "dependencies": {
+ "monaco-editor": "^0.48.0",
+ "solid-monaco": "^0.x.x"
+ }
+}
+```
+
+**2. Environment Configuration**
+
+Create environment-specific configurations:
+
+```bash
+# .env (development)
+# This allows vite to serve monaco assets directly from
+# node_modules when in dev mode.
+VITE_MONACO_ASSETS_PATH=/node_modules/monaco-editor/dev/vs
+
+# .env.production
+# Configure the path where minified monaco assets will be
+# found. See `monacoAssetsPlugin` below.
+VITE_MONACO_ASSETS_PATH=/monaco-assets/vs
+```
+
+**3. Vite Configuration**
+
+Add a custom plugin to copy Monaco assets during build:
+
+```typescript
+// vite.config.ts
+import { cpSync, existsSync } from 'node:fs';
+import { join } from 'node:path';
+import { defineConfig } from 'vite';
+import solidPlugin from 'vite-plugin-solid';
+
+const monacoAssetsPlugin = () => {
+ return {
+ name: 'monaco-assets-plugin',
+ generateBundle() {
+ // Copy Monaco Editor assets to build output directory
+ const monacoSrc = join(process.cwd(), 'node_modules/monaco-editor/min/vs');
+ const buildDest = join(process.cwd(), 'dist/monaco-assets/vs');
+
+ if (existsSync(monacoSrc)) {
+ cpSync(monacoSrc, buildDest, { recursive: true });
+ console.log('✓ Monaco Editor assets copied to dist directory');
+ }
+ },
+ };
+};
+
+export default defineConfig({
+ plugins: [solidPlugin(), monacoAssetsPlugin()],
+ // ... other config
+});
+```
+
+**4. Component Usage**
+
+Use the environment variable to configure asset loading:
+
+```jsx
+import { MonacoDiffEditor } from 'solid-monaco';
+
+function MyDiffEditor() {
+ const configureDiffEditor = monaco => {
+ // Configure JSON language features
+ monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
+ validate: true,
+ allowComments: false,
+ schemas: [],
+ enableSchemaRequest: false,
+ });
+
+ // Configure JSON formatting
+ monaco.languages.json.jsonDefaults.setModeConfiguration({
+ documentFormattingEdits: true,
+ documentRangeFormattingEdits: true,
+ completionItems: true,
+ hovers: true,
+ documentSymbols: true,
+ tokens: true,
+ colors: true,
+ foldingRanges: true,
+ diagnostics: true,
+ selectionRanges: true,
+ });
+ };
+
+ return (
+
+ );
+}
+```
+
+### Build Optimization
+
+#### Bundle Splitting
+
+Configure Vite to split Monaco into separate chunks for better loading performance:
+
+```typescript
+// vite.config.ts
+export default defineConfig({
+ build: {
+ rollupOptions: {
+ output: {
+ manualChunks: {
+ 'solid-monaco': ['solid-monaco'],
+ 'monaco-editor': ['monaco-editor'],
+ },
+ },
+ },
+ },
+});
+```
+
+### Troubleshooting
+
+#### Common Issues
+
+**Monaco assets not loading in production:**
+
+- Ensure `monaco-editor` is in `dependencies`, not `devDependencies`
+- Verify the Vite plugin is copying assets to the correct build directory
+- Check that `VITE_MONACO_ASSETS_PATH` matches your actual asset path
+
+**Large bundle size:**
+
+- Use `import type` for Monaco types to avoid bundling the entire library
+- Configure bundle splitting to separate Monaco into its own chunk
+- Consider lazy loading Monaco for non-critical editor instances
+
+**Web workers failing:**
+
+- Ensure web worker files are accessible at the configured asset path
+- Check browser console for 404 errors on worker files
+- Verify CORS settings if serving assets from a different domain
## Contributing
diff --git a/dev/vite.config.ts b/dev/vite.config.ts
index a198a70..d43f8c3 100644
--- a/dev/vite.config.ts
+++ b/dev/vite.config.ts
@@ -10,7 +10,7 @@ export default defineConfig({
plugins: [
solidPlugin(),
{
- name: 'Reaplace env variables',
+ name: 'Replace env variables',
transform(code, id) {
if (id.includes('node_modules')) {
return code
diff --git a/src/MonacoDiffEditor.tsx b/src/MonacoDiffEditor.tsx
index 8967f36..4445c2a 100644
--- a/src/MonacoDiffEditor.tsx
+++ b/src/MonacoDiffEditor.tsx
@@ -1,5 +1,5 @@
import { createSignal, createEffect, onCleanup, JSX, onMount, mergeProps, on } from 'solid-js'
-import * as monacoEditor from 'monaco-editor'
+import type * as monacoEditor from 'monaco-editor'
import loader, { Monaco } from '@monaco-editor/loader'
import { Loader } from './Loader'
import { MonacoContainer } from './MonacoContainer'
@@ -24,10 +24,12 @@ export interface MonacoDiffEditorProps {
overrideServices?: monacoEditor.editor.IEditorOverrideServices
width?: string
height?: string
- options?: monacoEditor.editor.IStandaloneEditorConstructionOptions
+ options?: monacoEditor.editor.IStandaloneDiffEditorConstructionOptions
saveViewState?: boolean
loaderParams?: LoaderParams
+
onChange?: (value: string) => void
+ onBeforeMount?: (monaco: Monaco) => void
onMount?: (monaco: Monaco, editor: monacoEditor.editor.IStandaloneDiffEditor) => void
onBeforeUnmount?: (monaco: Monaco, editor: monacoEditor.editor.IStandaloneDiffEditor) => void
}
@@ -50,17 +52,25 @@ export const MonacoDiffEditor = (inputProps: MonacoDiffEditorProps) => {
const [editor, setEditor] = createSignal()
let abortInitialization: (() => void) | undefined
- let monacoOnChangeSubscription: any
+ let monacoOnChangeSubscription: monacoEditor.IDisposable | undefined
let isOnChangeSuppressed = false
onMount(async () => {
- loader.config(inputProps.loaderParams ?? { monaco: monacoEditor })
+ loader.config(inputProps.loaderParams ?? {
+ paths: {
+ vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.48.0/min/vs'
+ }
+ })
const loadMonaco = loader.init()
abortInitialization = () => loadMonaco.cancel()
try {
const monaco = await loadMonaco
+
+ // Call beforeMount callback before editor creation
+ props.onBeforeMount?.(monaco)
+
const editor = createEditor(monaco)
setMonaco(monaco)
setEditor(editor)
@@ -97,7 +107,7 @@ export const MonacoDiffEditor = (inputProps: MonacoDiffEditorProps) => {
createEffect(
on(
() => props.modified,
- modified => {
+ (modified: string | undefined) => {
const _editor = editor()?.getModifiedEditor()
if (!_editor || typeof modified === 'undefined') {
return
@@ -130,7 +140,7 @@ export const MonacoDiffEditor = (inputProps: MonacoDiffEditorProps) => {
createEffect(
on(
() => props.original,
- original => {
+ (original: string | undefined) => {
const _editor = editor()?.getOriginalEditor()
if (!_editor || typeof original === 'undefined') {
return
@@ -147,7 +157,7 @@ export const MonacoDiffEditor = (inputProps: MonacoDiffEditorProps) => {
createEffect(
on(
() => props.options,
- options => {
+ (options: monacoEditor.editor.IStandaloneDiffEditorConstructionOptions | undefined) => {
editor()?.updateOptions(options ?? {})
},
{ defer: true },
@@ -157,7 +167,7 @@ export const MonacoDiffEditor = (inputProps: MonacoDiffEditorProps) => {
createEffect(
on(
() => props.theme,
- theme => {
+ (theme: monacoEditor.editor.BuiltinTheme | string) => {
monaco()?.editor.setTheme(theme)
},
{ defer: true },
@@ -167,7 +177,7 @@ export const MonacoDiffEditor = (inputProps: MonacoDiffEditorProps) => {
createEffect(
on(
() => props.originalLanguage,
- language => {
+ (language: string | undefined) => {
const model = editor()?.getModel()
if (!language || !model) {
return
@@ -182,7 +192,7 @@ export const MonacoDiffEditor = (inputProps: MonacoDiffEditorProps) => {
createEffect(
on(
() => props.modifiedLanguage,
- language => {
+ (language: string | undefined) => {
const model = editor()?.getModel()
if (!language || !model) {
return
@@ -247,6 +257,7 @@ export const MonacoDiffEditor = (inputProps: MonacoDiffEditorProps) => {
)
const createEditor = (monaco: Monaco) => {
+
const originalModel = getOrCreateModel(
monaco,
props.original ?? '',
@@ -261,7 +272,7 @@ export const MonacoDiffEditor = (inputProps: MonacoDiffEditorProps) => {
)
const editor = monaco.editor.createDiffEditor(
- containerRef,
+ containerRef!,
{
automaticLayout: true,
...props.options,
diff --git a/src/MonacoEditor.test.tsx b/src/MonacoEditor.test.tsx
index 0d8346d..324f097 100644
--- a/src/MonacoEditor.test.tsx
+++ b/src/MonacoEditor.test.tsx
@@ -2,7 +2,6 @@ import { createRoot } from 'solid-js'
import { describe, expect, it } from 'vitest'
import { MonacoEditor } from '../src'
-// TODO: add real tests
describe('MonacoEditor', () => {
it('renders a MonacoEditor component', async () => {
createRoot(() => {
diff --git a/src/MonacoEditor.tsx b/src/MonacoEditor.tsx
index dc75b4e..184f9e4 100644
--- a/src/MonacoEditor.tsx
+++ b/src/MonacoEditor.tsx
@@ -1,5 +1,5 @@
import { createSignal, createEffect, onCleanup, JSX, onMount, mergeProps, on } from 'solid-js'
-import * as monacoEditor from 'monaco-editor'
+import type * as monacoEditor from 'monaco-editor'
import loader, { Monaco } from '@monaco-editor/loader'
import { Loader } from './Loader'
import { MonacoContainer } from './MonacoContainer'
@@ -11,6 +11,7 @@ const viewStates = new Map()
export interface MonacoEditorProps {
language?: string
value?: string
+ line?: number
loadingState?: JSX.Element
class?: string
theme?: monacoEditor.editor.BuiltinTheme | string
@@ -22,8 +23,10 @@ export interface MonacoEditorProps {
saveViewState?: boolean
loaderParams?: LoaderParams
onChange?: (value: string, event: monacoEditor.editor.IModelContentChangedEvent) => void
+ onBeforeMount?: (monaco: Monaco) => void
onMount?: (monaco: Monaco, editor: monacoEditor.editor.IStandaloneCodeEditor) => void
onBeforeUnmount?: (monaco: Monaco, editor: monacoEditor.editor.IStandaloneCodeEditor) => void
+ onValidate?: (markers: monacoEditor.editor.IMarker[]) => void
}
export const MonacoEditor = (inputProps: MonacoEditorProps) => {
@@ -45,26 +48,55 @@ export const MonacoEditor = (inputProps: MonacoEditorProps) => {
let abortInitialization: (() => void) | undefined
let monacoOnChangeSubscription: any
+ let validationSubscription: monacoEditor.IDisposable | undefined
let isOnChangeSuppressed = false
onMount(async () => {
- loader.config(inputProps.loaderParams ?? { monaco: monacoEditor })
+ loader.config(inputProps.loaderParams ?? {
+ paths: {
+ vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.48.0/min/vs'
+ }
+ })
const loadMonaco = loader.init()
abortInitialization = () => loadMonaco.cancel()
try {
const monaco = await loadMonaco
+
+ // Call beforeMount callback before editor creation
+ props.onBeforeMount?.(monaco)
+
const editor = createEditor(monaco)
setMonaco(monaco)
setEditor(editor)
+
+ // Handle initial line positioning
+ if (props.line !== undefined) {
+ editor.revealLine(props.line)
+ }
+
props.onMount?.(monaco, editor)
- monacoOnChangeSubscription = editor.onDidChangeModelContent(event => {
+ monacoOnChangeSubscription = editor.onDidChangeModelContent((event: monacoEditor.editor.IModelContentChangedEvent) => {
if (!isOnChangeSuppressed) {
props.onChange?.(editor.getValue(), event)
}
})
+
+ // Setup validation subscription if onValidate is provided
+ if (props.onValidate) {
+ validationSubscription = monaco.editor.onDidChangeMarkers((uris: readonly monacoEditor.Uri[]) => {
+ const editorUri = editor.getModel()?.uri
+ if (editorUri) {
+ const currentEditorHasMarkerChanges = uris.find((uri: monacoEditor.Uri) => uri.path === editorUri.path)
+ if (currentEditorHasMarkerChanges) {
+ const markers = monaco.editor.getModelMarkers({ resource: editorUri })
+ props.onValidate!(markers)
+ }
+ }
+ })
+ }
} catch (error: any) {
if (error?.type === 'cancelation') {
return
@@ -83,6 +115,7 @@ export const MonacoEditor = (inputProps: MonacoEditorProps) => {
props.onBeforeUnmount?.(monaco()!, _editor)
monacoOnChangeSubscription?.dispose()
+ validationSubscription?.dispose()
_editor.getModel()?.dispose()
_editor.dispose()
})
@@ -90,7 +123,7 @@ export const MonacoEditor = (inputProps: MonacoEditorProps) => {
createEffect(
on(
() => props.value,
- value => {
+ (value: string | undefined) => {
const _editor = editor()
if (!_editor || typeof value === 'undefined') {
return
@@ -123,7 +156,7 @@ export const MonacoEditor = (inputProps: MonacoEditorProps) => {
createEffect(
on(
() => props.options,
- options => {
+ (options: monacoEditor.editor.IStandaloneEditorConstructionOptions | undefined) => {
editor()?.updateOptions(options ?? {})
},
{ defer: true },
@@ -133,7 +166,7 @@ export const MonacoEditor = (inputProps: MonacoEditorProps) => {
createEffect(
on(
() => props.theme,
- theme => {
+ (theme: monacoEditor.editor.BuiltinTheme | string) => {
monaco()?.editor.setTheme(theme)
},
{ defer: true },
@@ -143,7 +176,7 @@ export const MonacoEditor = (inputProps: MonacoEditorProps) => {
createEffect(
on(
() => props.language,
- language => {
+ (language: string | undefined) => {
const model = editor()?.getModel()
if (!language || !model) {
return
@@ -158,7 +191,7 @@ export const MonacoEditor = (inputProps: MonacoEditorProps) => {
createEffect(
on(
() => props.path,
- (path, prevPath) => {
+ (path: string | undefined, prevPath: string | undefined) => {
const _monaco = monaco()
if (!_monaco) {
return
@@ -180,11 +213,24 @@ export const MonacoEditor = (inputProps: MonacoEditorProps) => {
),
)
- const createEditor = (monaco: Monaco) => {
+ createEffect(
+ on(
+ () => props.line,
+ (line: number | undefined) => {
+ const currentEditor = editor()
+ if (line !== undefined && currentEditor) {
+ currentEditor.revealLine(line)
+ }
+ },
+ { defer: true },
+ ),
+ )
+
+ const createEditor = (monaco: Monaco) => {
const model = getOrCreateModel(monaco, props.value ?? '', props.language, props.path)
return monaco.editor.create(
- containerRef,
+ containerRef!,
{
model: model,
automaticLayout: true,