GitHub |
diff --git a/example/scripts/typescript.ts b/example/scripts/typescript.ts
new file mode 100644
index 0000000..5d31fa5
--- /dev/null
+++ b/example/scripts/typescript.ts
@@ -0,0 +1,264 @@
+import { autocompletion, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete'
+import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'
+import { javascript } from '@codemirror/lang-javascript'
+import { foldGutter, foldKeymap } from '@codemirror/language'
+import { linter, lintKeymap } from '@codemirror/lint'
+import { oneDark } from '@codemirror/theme-one-dark'
+import { EditorView, hoverTooltip, keymap, lineNumbers } from '@codemirror/view'
+import { createNpmFileSystem } from '@volar/jsdelivr'
+import {
+ createLanguage,
+ createLanguageService,
+ createUriMap,
+ type LanguageServiceEnvironment,
+ type ProjectContext,
+ type SourceScript
+} from '@volar/language-service'
+import { createLanguageServiceHost, createSys, resolveFileLanguageId } from '@volar/typescript'
+import {
+ createCompletionSource,
+ createHoverTooltipSource,
+ createLintSource,
+ dispatchTextEdits,
+ getTextDocument,
+ textDocument
+} from 'codemirror-languageservice'
+import { toDom } from 'hast-util-to-dom'
+import { fromMarkdown } from 'mdast-util-from-markdown'
+import { toHast } from 'mdast-util-to-hast'
+import * as ts from 'typescript'
+import { create as createTypeScriptPlugins } from 'volar-service-typescript'
+import { type TextDocument } from 'vscode-languageserver-textdocument'
+import { URI } from 'vscode-uri'
+
+/**
+ * Convert markdown content to a DOM node.
+ *
+ * @param markdown
+ * The markdown content.
+ * @returns
+ * The DOM node that represents the markdown.
+ */
+function markdownToDom(markdown: string): Node {
+ const mdast = fromMarkdown(markdown)
+ const hast = toHast(mdast)
+ const html = toDom(hast, { fragment: true })
+ return html
+}
+
+const docUri = 'file:///example.tsx'
+const docText = `import { ChangeEventHandler, ReactNode, useState } from 'react'
+
+export namespace Greeting {
+ export interface Props {
+ /**
+ * The name of the person to greet.
+ */
+ name: string
+ }
+}
+
+/**
+ * Render a greeting for a person.
+ */
+export function Greeting({ name }: Greeting.Props): ReactNode {
+ console.log('Hello', \`$\{name}!\`)
+
+ return (
+
+ Hello {name}!
+
+ )
+}
+
+/**
+ * Render the app.
+ */
+export function App(): ReactNode {
+ const [name, setName] = useState('Volar')
+
+ const handleChange: ChangeEventHandler = (event) => {
+ setName(event.currentTarget.name)
+ }
+
+ return (
+
+
+
+
+ )
+}
+`
+
+const env: LanguageServiceEnvironment = {
+ fs: createNpmFileSystem(),
+ workspaceFolders: []
+}
+const uriConverter = {
+ asUri: URI.file,
+ asFileName(uri: URI) {
+ return uri.path
+ }
+}
+const sys = createSys(ts.sys, env, () => '', uriConverter)
+const syncDocuments =
+ createUriMap<[TextDocument, number | undefined, ts.IScriptSnapshot | undefined]>()
+const fsFileSnapshots = createUriMap<[number | undefined, ts.IScriptSnapshot | undefined]>()
+const language = createLanguage(
+ [
+ {
+ getLanguageId: (uri) => syncDocuments.get(uri)?.[0].languageId
+ },
+ {
+ getLanguageId: (uri) => resolveFileLanguageId(uri.path)
+ }
+ ],
+ createUriMap
>(false),
+ (uri, includeFsFiles) => {
+ let snapshot: ts.IScriptSnapshot | undefined
+
+ const syncDocument = syncDocuments.get(uri)
+ if (syncDocument) {
+ if (!syncDocument[2] || syncDocument[0].version !== syncDocument[1]) {
+ syncDocument[1] = syncDocument[0].version
+ syncDocument[2] = ts.ScriptSnapshot.fromString(syncDocument[0].getText())
+ }
+ snapshot = syncDocument[2]
+ } else if (includeFsFiles) {
+ const cache = fsFileSnapshots.get(uri)
+ const fileName = uriConverter.asFileName(uri)
+ const modifiedTime = sys.getModifiedTime?.(fileName)?.valueOf()
+ if (!cache || cache[0] !== modifiedTime) {
+ if (sys.fileExists(fileName)) {
+ const text = sys.readFile(fileName)
+ fsFileSnapshots.set(uri, [
+ modifiedTime,
+ text === undefined ? undefined : ts.ScriptSnapshot.fromString(text)
+ ])
+ } else {
+ fsFileSnapshots.set(uri, [modifiedTime, undefined])
+ }
+ }
+ snapshot = fsFileSnapshots.get(uri)?.[1]
+ }
+
+ if (snapshot) {
+ language.scripts.set(uri, snapshot)
+ } else {
+ language.scripts.delete(uri)
+ }
+ }
+)
+const project: ProjectContext = {
+ typescript: {
+ configFileName: '',
+ sys,
+ uriConverter,
+ ...createLanguageServiceHost(ts, sys, language, URI.file, {
+ getCompilationSettings() {
+ return {
+ checkJs: true,
+ jsx: ts.JsxEmit.ReactJSX,
+ module: ts.ModuleKind.Preserve,
+ target: ts.ScriptTarget.ESNext
+ }
+ },
+ getCurrentDirectory() {
+ return sys.getCurrentDirectory()
+ },
+ getScriptFileNames() {
+ return [docUri.slice('file://'.length)]
+ }
+ })
+ }
+}
+const languageService = createLanguageService(
+ language,
+ createTypeScriptPlugins(ts, {}),
+ env,
+ project
+)
+
+/**
+ * Synchronize a document from CodeMirror into Volar.
+ *
+ * @param document
+ * The document to synchronize.
+ * @returns
+ * The URI that matches the document.
+ */
+function sync(document: TextDocument): URI {
+ const uri = URI.parse(document.uri)
+ if (syncDocuments.has(uri)) {
+ syncDocuments.get(uri)![0] = document
+ } else {
+ syncDocuments.set(uri, [document, undefined, undefined])
+ }
+ return uri
+}
+
+const completionOptions: createCompletionSource.Options = {
+ section: 'TypeScript',
+ markdownToDom,
+ triggerCharacters: '":',
+ doComplete(document, position) {
+ return languageService.getCompletionItems(sync(document), position)
+ }
+}
+
+const hoverTooltipOptions: createHoverTooltipSource.Options = {
+ markdownToDom,
+ doHover(document, position) {
+ return languageService.getHover(sync(document), position)
+ }
+}
+
+const lintOptions: createLintSource.Options = {
+ doDiagnostics(document) {
+ return languageService.getDiagnostics(sync(document))
+ }
+}
+
+const view = new EditorView({
+ doc: docText,
+ parent: document.body,
+ extensions: [
+ textDocument(docUri),
+ javascript(),
+ lineNumbers(),
+ oneDark,
+ foldGutter(),
+ history(),
+ autocompletion({
+ override: [createCompletionSource(completionOptions)]
+ }),
+ keymap.of([
+ ...closeBracketsKeymap,
+ ...defaultKeymap,
+ ...historyKeymap,
+ ...foldKeymap,
+ ...completionKeymap,
+ ...lintKeymap
+ ]),
+ hoverTooltip(createHoverTooltipSource(hoverTooltipOptions)),
+ linter(createLintSource(lintOptions))
+ ]
+})
+
+document.getElementById('format-button')!.addEventListener('click', async () => {
+ const document = getTextDocument(view.state)
+ const text = document.getText()
+ const start = document.positionAt(0)
+ const end = document.positionAt(text.length)
+ const edits = await languageService.getDocumentFormattingEdits(
+ URI.parse(document.uri),
+ { insertSpaces: true, tabSize: 2 },
+ { start, end },
+ // eslint-disable-next-line unicorn/no-useless-undefined
+ undefined
+ )
+
+ if (edits) {
+ dispatchTextEdits(view, edits)
+ }
+})
diff --git a/example/typescript.html b/example/typescript.html
new file mode 100644
index 0000000..d450d15
--- /dev/null
+++ b/example/typescript.html
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+ CodeMirror language service MDX example
+
+
+
+
+
+
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
index ed2129b..036fdf4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,10 +14,14 @@
},
"devDependencies": {
"@codemirror/commands": "^6.0.0",
+ "@codemirror/lang-javascript": "^6.0.0",
"@codemirror/lang-json": "^6.0.0",
"@codemirror/theme-one-dark": "^6.0.0",
"@vitest/browser": "^2.0.0",
"@vitest/coverage-istanbul": "^2.0.0",
+ "@volar/jsdelivr": "~2.4.0",
+ "@volar/language-service": "~2.4.0",
+ "@volar/typescript": "~2.4.0",
"eslint": "^8.0.0",
"eslint-config-remcohaszing": "^10.0.0",
"hast-util-to-dom": "^4.0.0",
@@ -30,8 +34,10 @@
"typescript": "^5.0.0",
"vite": "^5.0.0",
"vitest": "^2.0.0",
+ "volar-service-typescript": "0.0.62",
"vscode-json-languageservice": "^5.0.0",
- "vscode-languageserver-types": "^3.0.0"
+ "vscode-languageserver-types": "^3.0.0",
+ "vscode-uri": "^3.0.0"
},
"funding": {
"url": "https://github.com/sponsors/remcohaszing"
@@ -485,7 +491,6 @@
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.17.0.tgz",
"integrity": "sha512-fdfj6e6ZxZf8yrkMHUSJJir7OJkHkZKaOZGzLWIYp2PZ3jd+d+UjG8zVPqJF6d3bKxkhvXTPan/UZ1t7Bqm0gA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
@@ -512,6 +517,22 @@
"@lezer/common": "^1.1.0"
}
},
+ "node_modules/@codemirror/lang-javascript": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.2.tgz",
+ "integrity": "sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.0.0",
+ "@codemirror/language": "^6.6.0",
+ "@codemirror/lint": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.17.0",
+ "@lezer/common": "^1.0.0",
+ "@lezer/javascript": "^1.0.0"
+ }
+ },
"node_modules/@codemirror/lang-json": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz",
@@ -542,7 +563,6 @@
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.1.tgz",
"integrity": "sha512-IZ0Y7S4/bpaunwggW2jYqwLuHj0QtESf5xcROewY6+lDNwZ/NzvR4t+vpYgg9m7V8UXLPYqG+lu3DF470E5Oxg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
@@ -1375,6 +1395,18 @@
"@lezer/common": "^1.0.0"
}
},
+ "node_modules/@lezer/javascript": {
+ "version": "1.4.17",
+ "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.17.tgz",
+ "integrity": "sha512-bYW4ctpyGK+JMumDApeUzuIezX01H76R1foD6LcRX224FWfyYit/HYxiPGDjXXe/wQWASjCvVGoukTH68+0HIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.2.0",
+ "@lezer/highlight": "^1.1.3",
+ "@lezer/lr": "^1.3.0"
+ }
+ },
"node_modules/@lezer/json": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.2.tgz",
@@ -2636,6 +2668,55 @@
"url": "https://opencollective.com/vitest"
}
},
+ "node_modules/@volar/jsdelivr": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/@volar/jsdelivr/-/jsdelivr-2.4.1.tgz",
+ "integrity": "sha512-Hf4tMghVaP8+GVVbkP8Vv++4TgPZzbv5KSKx9zuQmfBQHulttBzdXq9smPWJciW9CpmUEFkphXTGXfwiQrfTnQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@volar/language-core": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.1.tgz",
+ "integrity": "sha512-9AKhC7Qn2mQYxj7Dz3bVxeOk7gGJladhWixUYKef/o0o7Bm4an+A3XvmcTHVqZ8stE6lBVH++g050tBtJ4TZPQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/source-map": "2.4.1"
+ }
+ },
+ "node_modules/@volar/language-service": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/@volar/language-service/-/language-service-2.4.1.tgz",
+ "integrity": "sha512-Q3NVjZTAz0Vnco70Rgcryq2eDPWkFBdpzr84aYqOGvVC4SBjq1Wsx0d9NyA4seQHfHWwbZyzyviKRm+htyRlKg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.1",
+ "vscode-languageserver-protocol": "^3.17.5",
+ "vscode-languageserver-textdocument": "^1.0.11",
+ "vscode-uri": "^3.0.8"
+ }
+ },
+ "node_modules/@volar/source-map": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.1.tgz",
+ "integrity": "sha512-Xq6ep3OZg9xUqN90jEgB9ztX5SsTz1yiV8wiQbcYNjWkek+Ie3dc8l7AVt3EhDm9mSIR58oWczHkzM2H6HIsmQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@volar/typescript": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.1.tgz",
+ "integrity": "sha512-UoRzC0PXcwajFQTu8XxKSYNsWNBtVja6Y9gC8eLv7kYm+UEKJCcZ8g7dialsOYA0HKs3Vpg57MeCsawFLC6m9Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.1",
+ "path-browserify": "^1.0.1",
+ "vscode-uri": "^3.0.8"
+ }
+ },
"node_modules/@vscode/l10n": {
"version": "0.0.18",
"resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.18.tgz",
@@ -3496,8 +3577,7 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.3",
@@ -8089,6 +8169,13 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/path-browserify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -10248,6 +10335,16 @@
"node": ">=14.17"
}
},
+ "node_modules/typescript-auto-import-cache": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/typescript-auto-import-cache/-/typescript-auto-import-cache-0.3.3.tgz",
+ "integrity": "sha512-ojEC7+Ci1ij9eE6hp8Jl9VUNnsEKzztktP5gtYNRMrTmfXVwA1PITYYAkpxCvvupdSYa/Re51B6KMcv1CTZEUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.3.8"
+ }
+ },
"node_modules/unbox-primitive": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
@@ -11029,6 +11126,29 @@
}
}
},
+ "node_modules/volar-service-typescript": {
+ "version": "0.0.62",
+ "resolved": "https://registry.npmjs.org/volar-service-typescript/-/volar-service-typescript-0.0.62.tgz",
+ "integrity": "sha512-p7MPi71q7KOsH0eAbZwPBiKPp9B2+qrdHAd6VY5oTo9BUXatsOAdakTm9Yf0DUj6uWBAaOT01BSeVOPwucMV1g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-browserify": "^1.0.1",
+ "semver": "^7.6.2",
+ "typescript-auto-import-cache": "^0.3.3",
+ "vscode-languageserver-textdocument": "^1.0.11",
+ "vscode-nls": "^5.2.0",
+ "vscode-uri": "^3.0.8"
+ },
+ "peerDependencies": {
+ "@volar/language-service": "~2.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@volar/language-service": {
+ "optional": true
+ }
+ }
+ },
"node_modules/vscode-json-languageservice": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-5.4.0.tgz",
@@ -11074,6 +11194,13 @@
"integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==",
"license": "MIT"
},
+ "node_modules/vscode-nls": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-5.2.0.tgz",
+ "integrity": "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/vscode-uri": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz",
diff --git a/package.json b/package.json
index 6e022fe..79bc84d 100644
--- a/package.json
+++ b/package.json
@@ -45,10 +45,14 @@
},
"devDependencies": {
"@codemirror/commands": "^6.0.0",
+ "@codemirror/lang-javascript": "^6.0.0",
"@codemirror/lang-json": "^6.0.0",
"@codemirror/theme-one-dark": "^6.0.0",
"@vitest/browser": "^2.0.0",
"@vitest/coverage-istanbul": "^2.0.0",
+ "@volar/jsdelivr": "~2.4.0",
+ "@volar/language-service": "~2.4.0",
+ "@volar/typescript": "~2.4.0",
"eslint": "^8.0.0",
"eslint-config-remcohaszing": "^10.0.0",
"hast-util-to-dom": "^4.0.0",
@@ -61,7 +65,9 @@
"typescript": "^5.0.0",
"vite": "^5.0.0",
"vitest": "^2.0.0",
+ "volar-service-typescript": "0.0.62",
"vscode-json-languageservice": "^5.0.0",
- "vscode-languageserver-types": "^3.0.0"
+ "vscode-languageserver-types": "^3.0.0",
+ "vscode-uri": "^3.0.0"
}
}
diff --git a/vite.config.ts b/vite.config.ts
index ac4fc01..5de5f71 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -12,7 +12,11 @@ export default defineConfig({
emptyOutDir: true,
outDir: resolve('build'),
rollupOptions: {
- input: [resolve('example/index.html'), resolve('example/json.html')]
+ input: [
+ resolve('example/index.html'),
+ resolve('example/json.html'),
+ resolve('example/typescript.html')
+ ]
}
},
resolve: {