CodinGame / monaco-vscode-api

VSCode public API plugged on the monaco editor
MIT License
205 stars 29 forks source link

@codingame/monaco-vscode-api · monthly downloads npm version PRs welcome

NPM module that allows to use every part of VSCode, including the monaco editor

Installation

npm install vscode@npm:@codingame/monaco-vscode-api
npm install monaco-editor@npm:@codingame/monaco-vscode-editor-api
npm install -D @types/vscode

@codingame/monaco-vscode-api is installed as an alias to vscode to be able to run import * as vscode from 'vscode', similar to what is done inside a VSCode extension

@codingame/monaco-vscode-editor-api is installed as an alias to monaco-editor because it provides the same api as the official monaco-editor

Usage

Monaco standalone services

monaco-editor, as well as this library by default, uses standalone version or the vscode services, which are much simpler than the one used in VSCode.

You may want to provide your custom implementations of them. To do so, you can use the initialize method from vscode/services. Also, monaco-editor doesn't provide types for them, so this library exports them.

Example:

import { INotificationService, initialize } from 'vscode/services'

class MyCustomNotificationService implements INotificationService { ... }
await initialize({
  get [INotificationService.toString()] () {
    return new MyCustomNotificationService(...)
  }
})

Additionally, several packages that include the VSCode version of some services (with some glue to make it work with monaco) are published:

Usage:

import * as vscode from 'vscode'
import { initialize } from 'vscode/services'
import getEditorServiceOverride from '@codingame/monaco-vscode-editor-service-override'
import getConfigurationServiceOverride, { updateUserConfiguration, configurationRegistry } from '@codingame/monaco-vscode-configuration-service-override'

await initialize({
  ...getModelEditorServiceOverride((model, input, sideBySide) => {
    // Open a new editor here and return it
    // It will be called when for instance the user ctrl+click on an import
  }),
  ...getConfigurationServiceOverride()
})

updateUserConfiguration(`{
  "editor.fontSize": 12,
  "[java]": {
    "editor.fontSize": 15,
  }
}`)

Troubleshoot

initialize can only be called once ( and it should be called BEFORE creating your first editor).

Model creation

The official monaco-editor package provides a function to create models: monaco.editor.createModel.

This method creates a standalone model that cannot be found or used by any VSCode services.

The recommended way is to used the createModelReference method instead (added on top of the official monaco-editor api) which returns instead a reference to a model.

It has some pros:

The second argument of the method allows you to write the file content to the virtual filesystem in case the file wasn't registered in it beforehand.

before:

import * as monaco from 'monaco-editor'
const model = monaco.editor.createModel(...)
const editor = monaco.editor.create({ model, ... })

...

model.dispose()
editor.dispose()

after:

import * as monaco from 'monaco-editor'

const modelRef = await monaco.editor.createModelReference(...)

const editor = monaco.editor.create({ model: modelRef.object.textEditorModel })

...

await modelRef.object.save()

...

modelRef.dispose()
editor.dispose()

createModelReference return a reference to a model. The value is fetched from the memory filesystem (which is written if you provide the second argument). The reference can then be disposed, the model will only be disposed if there is no remaining references.

VSCode api usage

To be able to use the VSCode api directly from your code, you need to import vscode/localExtensionHost and the services to be initialized.

You will then be able to import it as if you were in a VSCode extension:

import * as vscode from 'vscode'
import 'vscode/localExtensionHost'

const range = new vscode.Range(...)
vscode.languages.registerCompletionItemProvider(...)

You can also register a new extension from its manifest:

import { registerExtension, initialize, ExtensionHostKind } from 'vscode/extensions'

await initialize()

const { registerFileUrl, getApi } = registerExtension({
  name: 'my-extension',
  publisher: 'someone',
  version: '1.0.0',
  engines: {
      vscode: '*'
  },
  contributes: {
  }
}, ExtensionHostKind.LocalProcess)

registerFileUrl('/file-extension-path.json', new URL('./file-real-path.json', import.meta.url).toString())

const vscode = await getApi()

vscode.languages.registerCompletionItemProvider(...)

Default vscode extensions

VSCode uses a bunch of default extensions. Most of them are used to load the default languages and grammars (see ttps://github.com/microsoft/vscode/tree/main/extensions).

This library bundles and publishes them and allows to import the ones you want:

import '@codingame/monaco-vscode-javascript-default-extension'
import '@codingame/monaco-vscode-json-default-extension'
...

Loading vsix file

VSCode extensions are bundled as vsix files. This library publishes a rollup plugin (vite-compatible) that allows to load a vsix file.

import vsixPlugin from '@codingame/monaco-vscode-rollup-vsix-plugin'
...
plugins: [
  ...,
  vsixPlugin()
]
import './extension.vsix'

Localization

This library also offers the possibility to localize vscode and the extensions in the supported languages. To do so, import one of the following packages before anything else:

⚠️ The language pack should be imported and loaded BEFORE anything else from monaco-editor or this library is loaded. Otherwise, some translations would be missing. ⚠️

Demo

Try it out on https://monaco-vscode-api.netlify.app/

There is a demo that showcases the service-override features. It allows to register contributions with the same syntaxes as in VSCode. It includes:

From CLI run:

# build monaco-vscode-api (the demo use it as a local dependency)
npm ci
npm run build
# start demo
cd demo
npm ci
npm start
# OR: for vite debug output
npm run start:debug

For the debug feature, also run:

npm run start:debugServer

⚠️ Building monaco-vscode-api is only supported on Linux or Mac. It you use Windows, have a look at WSL ⚠️

Remote agent

See vscode_server.md

Troubleshooting

Duplicate versions

Many packages are published with the same version, and almost all of them depend on the @codingame/monaco-vscode-api main package (with strict version range).

It is VERY important that only a single version of ALL the packages is installed, otherwise weird things can happen.

You can check that npm list vscode only lists a single version.

If you use Webpack

monaco-editor-webpack-plugin

Starting from v2, monaco-editor-webpack-plugin can't be used

Here's the alternative for each options:

exclude assets from loaders

Webpack makes all file go through all matching loaders. This libraries need to load a lot of internals resources, including HTML, svg and javascript files (for default extension codes).

We need webpack to let those file untouched:

Fortunately, all the assets are loaded via the new URL('asset.extension', import.meta.url) syntax, and webpack provide a way to exclude the file loaded that way: dependency: { not: ['url'] } see https://webpack.js.org/guides/asset-modules/

If you use Vite

This library uses a lot the new URL('asset.extension', import.meta.url) syntax which is supported by vite

While it works great in build mode (because rollup is used), there is some issues in watch mode:

There are workarounds for both:

import importMetaUrlPlugin from '@codingame/esbuild-import-meta-url-plugin'

{
  ...
  optimizeDeps: {
    esbuildOptions: {
      plugins: [importMetaUrlPlugin]
    }
  }
}

If using Angular and getting Not allowed to load local resource: errors

The short version: set up and use a custom webpack config file and add this under module:

parser: {
  javascript: {
    url: true,
  }
}

See this issue or this StackOverflow answer for more details, and this discussion for more context.

The typescript language features extension is not providing project-wide intellisense

The typescript language features extensions requires SharedArrayBuffer to enable project wide intellisense or only a per-file intellisense is provided.

It requires crossOriginIsolated to be true, which requires assets files to be servers with some specific headers:

At least thoses files should have the headers:

If adding those headers is not an options, you can have a look at https://github.com/gzuidhof/coi-serviceworker, but only if you are not using webviews as it introduces problems then.

History

This project was mainly created to make the implementation of monaco-languageclient more robust and maintainable.

monaco-languageclient uses vscode-languageclient which was built to run inside a VSCode extension. VSCode extensions communicate with the editor via an API they can import into their code.

The VSCode api exports:

The first implementations of monaco-languageclient were using a fake VSCode api implementation. The vscode-languageclient was hacked so the VSCode<->protocol object converters were mainly bypassed, so the fake VSCode api was receiving Language Server Protocol objects. Then the objects were transformed using custom transformers into Monaco objects to communicate with the monaco api.

This approach has some disadvantages:

With this library, it would be possible to plug vscode-languageclient directly on top of monaco, monaco-languageclient still helps to do so by: