deploy: current vibn theia state
Some checks failed
Playwright Tests / Playwright Tests (ubuntu-22.04, Node.js 22.x) (push) Has been cancelled
3PP License Check / 3PP License Check (11, 22.x, ubuntu-22.04) (push) Has been cancelled
Publish packages to NPM / Perform Publishing (push) Has been cancelled

Made-with: Cursor
This commit is contained in:
2026-02-27 12:01:08 -08:00
commit 8bb5110148
3782 changed files with 640947 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: [
'../../configs/build.eslintrc.json'
],
parserOptions: {
tsconfigRootDir: __dirname,
project: 'tsconfig.json'
}
};

113
packages/monaco/README.md Normal file
View File

@@ -0,0 +1,113 @@
<div align='center'>
<br />
<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />
<h2>ECLIPSE THEIA - MONACO EXTENSION</h2>
<hr />
</div>
## Description
The `@theia/monaco` extension contributes the integration of the [monaco-editor](https://microsoft.github.io/monaco-editor/).\
This includes:
- full-feature code editor
- diff-editor
- code snippets
- textmate grammars (theme registry, service)
## Monaco Uplifts
This package is intended to be the interface between the `@theia/monaco-editor-core` package, the project's bundling of the `monaco-editor-core` package published by the VSCode
team, and the rest of the application. When we uplift to a new version of `monaco-editor-core`, this package will need to be checked particularly thoroughly. To facilitate that
process, the steps for undertaking a Monaco uplift are outlined here.
### Setting up the VSCode side
1. Clone the VSCode repo and make sure you have the following remotes:
- <https://github.com/microsoft/vscode.git> - the official VSCode repo.
- <https://github.com/theia-ide/vscode.git> - Theia's fork.
2. Find the latest release tag in the official VSCode repo, and the most recent uplift branch in the Theia fork.
> At the time of this writing the latest release tag is `1.96.3`, and the uplift branch is `monaco-uplift-1.96.3`
3. Check out the release tag, cherry pick the tip of the uplift branch, and resolve any conflicts.
> As you resolve conflicts and make changes to the VSCode repo, make sure you end up with a single commit on the uplift branch to make it easier for the next person to rebase.
4. Try to build. At the moment, this means running `npm install` and `npm run gulp editor-distro`.
5. Fix any build errors that arise.
6. Change the version in `build/monaco/package.json`
#### Current State
- build/gulpfile.editor.js: various changes to modify the treeshaking and output destinations.
- build/lib/standalone.js/ts: changes to output sourcemaps etc. One small change to fix a build error due to having a directory named `model` and a file named `model.ts` in the same folder.
- src/vs/base/browser/dompurify/dompurify.js changes for CommonJS rather than ESM
- src/vs/base/common/marked/marked.js changes for CommonJS rather than ESM
### Setting up the Theia side
For initial testing, it's easier to point dependencies to your local VSCode.
1. Having built `monaco-editor-core` using the steps [above](#setting-up-the-vscode-side).
2. Find all references to `@theia/monaco-editor-core` in `package.json`s and replace their version with `"link:<path to your local build of monaco-editor-core>"`.
> Using `link:` means that if you subsequently make changes on the VSCode side, you only need to rebuild VSCode and then rebuild Theia to see the effects.
3. Delete your `node_modules` and `npm install` and build Theia.
4. Fix any build errors.
5. Uncomment the `bindMonacoPreferenceExtractor` function in `examples/api-samples/src/browser/monaco-editor-preferences/monaco-editor-preference-extractor.ts` and run the commands there. Fix the `EditorGeneratedPreferenceSchema` as necessary, and add or remove validations from the `MonacoFrontendApplicationContribution` as appropriate.
6. Look for comments that indicate forced types or other code smells that would prevent a build error from being thrown where it should be thrown and check that the assertion still applies.
> If you add these, mark them with @monaco-uplift - that'll make them easier to find in the future. Better: if you can remove them, do! Typically, the cause is mixing imports from
private API and public API. Often public API fails to satisfy private declarations.
7. Test the application thoroughly - make sure everything's still working.
> It may also be necessary to update our various `vscode` dependencies to match the current state of VSCode. It may not be necessary to upgrade all (or any) of these to successfully adopt a new Monaco version, but if something is misbehaving inexplicably, checking dependencies is a reasonable place to start. Check on:
>
> - `vscode-debugprotocol`
> - `vscode-languageserver-protocol`
> - `vscode-oniguruma`
> - `vscode-proxy-agent`
> - `vscode-ripgrep`
> - `vscode-textmate`
> - `vscode-uri`
### Publishing for testing
Once you believe that everything is in working order, you'll need to publish the new `@theia/monaco-editor-core` for testing. The instructions for doing so are
[here](https://github.com/theia-ide/vscode/wiki/Publish-%60@theia-monaco-editor-core%60). Once the package is published, point your `package.json`s at the testing version and make
sure everything still works, then make a PR.
## Additional Information
- [API documentation for `@theia/monaco`](https://eclipse-theia.github.io/theia/docs/next/modules/_theia_monaco.html)
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
- [Theia - Website](https://theia-ide.org/)
## License
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
## Trademark
"Theia" is a trademark of the Eclipse Foundation
<https://www.eclipse.org/theia>
# Theia - Monaco Extension
See [here](https://www.theia-ide.org/doc/index.html) for a detailed documentation.
## License
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)

View File

@@ -0,0 +1,205 @@
{
"$schema": "vscode://schemas/color-theme",
"name": "Dark+",
"include": "./dark_vs.json",
"tokenColors": [
{
"name": "Function declarations",
"scope": [
"entity.name.function",
"support.function",
"support.constant.handlebars",
"source.powershell variable.other.member",
"entity.name.operator.custom-literal"
],
"settings": {
"foreground": "#DCDCAA"
}
},
{
"name": "Types declaration and references",
"scope": [
"support.class",
"support.type",
"entity.name.type",
"entity.name.namespace",
"entity.other.attribute",
"entity.name.scope-resolution",
"entity.name.class",
"storage.type.numeric.go",
"storage.type.byte.go",
"storage.type.boolean.go",
"storage.type.string.go",
"storage.type.uintptr.go",
"storage.type.error.go",
"storage.type.rune.go",
"storage.type.cs",
"storage.type.generic.cs",
"storage.type.modifier.cs",
"storage.type.variable.cs",
"storage.type.annotation.java",
"storage.type.generic.java",
"storage.type.java",
"storage.type.object.array.java",
"storage.type.primitive.array.java",
"storage.type.primitive.java",
"storage.type.token.java",
"storage.type.groovy",
"storage.type.annotation.groovy",
"storage.type.parameters.groovy",
"storage.type.generic.groovy",
"storage.type.object.array.groovy",
"storage.type.primitive.array.groovy",
"storage.type.primitive.groovy"
],
"settings": {
"foreground": "#4EC9B0"
}
},
{
"name": "Types declaration and references, TS grammar specific",
"scope": [
"meta.type.cast.expr",
"meta.type.new.expr",
"support.constant.math",
"support.constant.dom",
"support.constant.json",
"entity.other.inherited-class",
"punctuation.separator.namespace.ruby"
],
"settings": {
"foreground": "#4EC9B0"
}
},
{
"name": "Control flow / Special keywords",
"scope": [
"keyword.control",
"source.cpp keyword.operator.new",
"keyword.operator.delete",
"keyword.other.using",
"keyword.other.directive.using",
"keyword.other.operator",
"entity.name.operator"
],
"settings": {
"foreground": "#C586C0"
}
},
{
"name": "Variable and parameter name",
"scope": [
"variable",
"meta.definition.variable.name",
"support.variable",
"entity.name.variable",
"constant.other.placeholder"
],
"settings": {
"foreground": "#9CDCFE"
}
},
{
"name": "Constants and enums",
"scope": [
"variable.other.constant",
"variable.other.enummember"
],
"settings": {
"foreground": "#4FC1FF"
}
},
{
"name": "Object keys, TS grammar specific",
"scope": [
"meta.object-literal.key"
],
"settings": {
"foreground": "#9CDCFE"
}
},
{
"name": "CSS property value",
"scope": [
"support.constant.property-value",
"support.constant.font-name",
"support.constant.media-type",
"support.constant.media",
"constant.other.color.rgb-value",
"constant.other.rgb-value",
"support.constant.color"
],
"settings": {
"foreground": "#CE9178"
}
},
{
"name": "Regular expression groups",
"scope": [
"punctuation.definition.group.regexp",
"punctuation.definition.group.assertion.regexp",
"punctuation.definition.character-class.regexp",
"punctuation.character.set.begin.regexp",
"punctuation.character.set.end.regexp",
"keyword.operator.negation.regexp",
"support.other.parenthesis.regexp"
],
"settings": {
"foreground": "#CE9178"
}
},
{
"scope": [
"constant.character.character-class.regexp",
"constant.other.character-class.set.regexp",
"constant.other.character-class.regexp",
"constant.character.set.regexp"
],
"settings": {
"foreground": "#d16969"
}
},
{
"scope": [
"keyword.operator.or.regexp",
"keyword.control.anchor.regexp"
],
"settings": {
"foreground": "#DCDCAA"
}
},
{
"scope": "keyword.operator.quantifier.regexp",
"settings": {
"foreground": "#d7ba7d"
}
},
{
"scope": [
"constant.character",
"constant.other.option"
],
"settings": {
"foreground": "#569cd6"
}
},
{
"scope": "constant.character.escape",
"settings": {
"foreground": "#d7ba7d"
}
},
{
"scope": "entity.name.label",
"settings": {
"foreground": "#C8C8C8"
}
}
],
"semanticTokenColors": {
"newOperator": "#C586C0",
"stringLiteral": "#ce9178",
"customLiteral": "#DCDCAA",
"numberLiteral": "#b5cea8"
}
}

View File

@@ -0,0 +1,5 @@
{
"$schema": "vscode://schemas/color-theme",
"name": "Dark (Theia)",
"include": "./dark_plus.json"
}

View File

@@ -0,0 +1,411 @@
{
"$schema": "vscode://schemas/color-theme",
"name": "Dark (Visual Studio)",
"colors": {
"checkbox.border": "#6B6B6B",
"editor.background": "#1E1E1E",
"editor.foreground": "#D4D4D4",
"editor.inactiveSelectionBackground": "#3A3D41",
"editorIndentGuide.background1": "#404040",
"editorIndentGuide.activeBackground1": "#707070",
"editor.selectionHighlightBackground": "#ADD6FF26",
"list.dropBackground": "#383B3D",
"activityBarBadge.background": "#007ACC",
"sideBarTitle.foreground": "#BBBBBB",
"input.placeholderForeground": "#A6A6A6",
"menu.background": "#252526",
"menu.foreground": "#CCCCCC",
"menu.separatorBackground": "#454545",
"menu.border": "#454545",
"menu.selectionBackground": "#0078d4",
"statusBarItem.remoteForeground": "#FFF",
"statusBarItem.remoteBackground": "#16825D",
"ports.iconRunningProcessForeground": "#369432",
"sideBarSectionHeader.background": "#0000",
"sideBarSectionHeader.border": "#ccc3",
"tab.selectedBackground": "#222222",
"tab.selectedForeground": "#ffffffa0",
"tab.lastPinnedBorder": "#ccc3",
"list.activeSelectionIconForeground": "#FFF",
"terminal.inactiveSelectionBackground": "#3A3D41",
"widget.border": "#303031",
"actionBar.toggledBackground": "#383a49"
},
"tokenColors": [
{
"scope": [
"meta.embedded",
"source.groovy.embedded",
"string meta.image.inline.markdown",
"variable.legacy.builtin.python"
],
"settings": {
"foreground": "#D4D4D4"
}
},
{
"scope": "emphasis",
"settings": {
"fontStyle": "italic"
}
},
{
"scope": "strong",
"settings": {
"fontStyle": "bold"
}
},
{
"scope": "header",
"settings": {
"foreground": "#000080"
}
},
{
"scope": "comment",
"settings": {
"foreground": "#6A9955"
}
},
{
"scope": "constant.language",
"settings": {
"foreground": "#569cd6"
}
},
{
"scope": [
"constant.numeric",
"variable.other.enummember",
"keyword.operator.plus.exponent",
"keyword.operator.minus.exponent"
],
"settings": {
"foreground": "#b5cea8"
}
},
{
"scope": "constant.regexp",
"settings": {
"foreground": "#646695"
}
},
{
"scope": "entity.name.tag",
"settings": {
"foreground": "#569cd6"
}
},
{
"scope": [
"entity.name.tag.css",
"entity.name.tag.less"
],
"settings": {
"foreground": "#d7ba7d"
}
},
{
"scope": "entity.other.attribute-name",
"settings": {
"foreground": "#9cdcfe"
}
},
{
"scope": [
"entity.other.attribute-name.class.css",
"source.css entity.other.attribute-name.class",
"entity.other.attribute-name.id.css",
"entity.other.attribute-name.parent-selector.css",
"entity.other.attribute-name.parent.less",
"source.css entity.other.attribute-name.pseudo-class",
"entity.other.attribute-name.pseudo-element.css",
"source.css.less entity.other.attribute-name.id",
"entity.other.attribute-name.scss"
],
"settings": {
"foreground": "#d7ba7d"
}
},
{
"scope": "invalid",
"settings": {
"foreground": "#f44747"
}
},
{
"scope": "markup.underline",
"settings": {
"fontStyle": "underline"
}
},
{
"scope": "markup.bold",
"settings": {
"fontStyle": "bold",
"foreground": "#569cd6"
}
},
{
"scope": "markup.heading",
"settings": {
"fontStyle": "bold",
"foreground": "#569cd6"
}
},
{
"scope": "markup.italic",
"settings": {
"fontStyle": "italic"
}
},
{
"scope": "markup.strikethrough",
"settings": {
"fontStyle": "strikethrough"
}
},
{
"scope": "markup.inserted",
"settings": {
"foreground": "#b5cea8"
}
},
{
"scope": "markup.deleted",
"settings": {
"foreground": "#ce9178"
}
},
{
"scope": "markup.changed",
"settings": {
"foreground": "#569cd6"
}
},
{
"scope": "punctuation.definition.quote.begin.markdown",
"settings": {
"foreground": "#6A9955"
}
},
{
"scope": "punctuation.definition.list.begin.markdown",
"settings": {
"foreground": "#6796e6"
}
},
{
"scope": "markup.inline.raw",
"settings": {
"foreground": "#ce9178"
}
},
{
"name": "brackets of XML/HTML tags",
"scope": "punctuation.definition.tag",
"settings": {
"foreground": "#808080"
}
},
{
"scope": [
"meta.preprocessor",
"entity.name.function.preprocessor"
],
"settings": {
"foreground": "#569cd6"
}
},
{
"scope": "meta.preprocessor.string",
"settings": {
"foreground": "#ce9178"
}
},
{
"scope": "meta.preprocessor.numeric",
"settings": {
"foreground": "#b5cea8"
}
},
{
"scope": "meta.structure.dictionary.key.python",
"settings": {
"foreground": "#9cdcfe"
}
},
{
"scope": "meta.diff.header",
"settings": {
"foreground": "#569cd6"
}
},
{
"scope": "storage",
"settings": {
"foreground": "#569cd6"
}
},
{
"scope": "storage.type",
"settings": {
"foreground": "#569cd6"
}
},
{
"scope": [
"storage.modifier",
"keyword.operator.noexcept"
],
"settings": {
"foreground": "#569cd6"
}
},
{
"scope": [
"string",
"meta.embedded.assembly"
],
"settings": {
"foreground": "#ce9178"
}
},
{
"scope": "string.tag",
"settings": {
"foreground": "#ce9178"
}
},
{
"scope": "string.value",
"settings": {
"foreground": "#ce9178"
}
},
{
"scope": "string.regexp",
"settings": {
"foreground": "#d16969"
}
},
{
"name": "String interpolation",
"scope": [
"punctuation.definition.template-expression.begin",
"punctuation.definition.template-expression.end",
"punctuation.section.embedded"
],
"settings": {
"foreground": "#569cd6"
}
},
{
"name": "Reset JavaScript string interpolation expression",
"scope": [
"meta.template.expression"
],
"settings": {
"foreground": "#d4d4d4"
}
},
{
"scope": [
"support.type.vendored.property-name",
"support.type.property-name",
"source.css variable",
"source.coffee.embedded"
],
"settings": {
"foreground": "#9cdcfe"
}
},
{
"scope": "keyword",
"settings": {
"foreground": "#569cd6"
}
},
{
"scope": "keyword.control",
"settings": {
"foreground": "#569cd6"
}
},
{
"scope": "keyword.operator",
"settings": {
"foreground": "#d4d4d4"
}
},
{
"scope": [
"keyword.operator.new",
"keyword.operator.expression",
"keyword.operator.cast",
"keyword.operator.sizeof",
"keyword.operator.alignof",
"keyword.operator.typeid",
"keyword.operator.alignas",
"keyword.operator.instanceof",
"keyword.operator.logical.python",
"keyword.operator.wordlike"
],
"settings": {
"foreground": "#569cd6"
}
},
{
"scope": "keyword.other.unit",
"settings": {
"foreground": "#b5cea8"
}
},
{
"scope": [
"punctuation.section.embedded.begin.php",
"punctuation.section.embedded.end.php"
],
"settings": {
"foreground": "#569cd6"
}
},
{
"scope": "support.function.git-rebase",
"settings": {
"foreground": "#9cdcfe"
}
},
{
"scope": "constant.sha.git-rebase",
"settings": {
"foreground": "#b5cea8"
}
},
{
"name": "coloring of the Java import and package identifiers",
"scope": [
"storage.modifier.import.java",
"variable.language.wildcard.java",
"storage.modifier.package.java"
],
"settings": {
"foreground": "#d4d4d4"
}
},
{
"name": "this.self",
"scope": "variable.language",
"settings": {
"foreground": "#569cd6"
}
}
],
"semanticHighlighting": true,
"semanticTokenColors": {
"newOperator": "#d4d4d4",
"stringLiteral": "#ce9178",
"customLiteral": "#D4D4D4",
"numberLiteral": "#b5cea8"
}
}

View File

@@ -0,0 +1,469 @@
{
"$schema": "vscode://schemas/color-theme",
"name": "Dark High Contrast",
"colors": {
"editor.background": "#000000",
"editor.foreground": "#FFFFFF",
"editorIndentGuide.background1": "#FFFFFF",
"editorIndentGuide.activeBackground1": "#FFFFFF",
"sideBarTitle.foreground": "#FFFFFF",
"selection.background": "#008000",
"editor.selectionBackground": "#FFFFFF",
"statusBarItem.remoteBackground": "#00000000",
"ports.iconRunningProcessForeground": "#FFFFFF",
"editorWhitespace.foreground": "#7c7c7c",
"actionBar.toggledBackground": "#383a49"
},
"tokenColors": [
{
"scope": [
"meta.embedded",
"source.groovy.embedded",
"string meta.image.inline.markdown",
"variable.legacy.builtin.python"
],
"settings": {
"foreground": "#FFFFFF"
}
},
{
"scope": "emphasis",
"settings": {
"fontStyle": "italic"
}
},
{
"scope": "strong",
"settings": {
"fontStyle": "bold"
}
},
{
"scope": "meta.diff.header",
"settings": {
"foreground": "#000080"
}
},
{
"scope": "comment",
"settings": {
"foreground": "#7ca668"
}
},
{
"scope": "constant.language",
"settings": {
"foreground": "#569cd6"
}
},
{
"scope": [
"constant.numeric",
"constant.other.color.rgb-value",
"constant.other.rgb-value",
"support.constant.color"
],
"settings": {
"foreground": "#b5cea8"
}
},
{
"scope": "constant.regexp",
"settings": {
"foreground": "#b46695"
}
},
{
"scope": "constant.character",
"settings": {
"foreground": "#569cd6"
}
},
{
"scope": "entity.name.tag",
"settings": {
"foreground": "#569cd6"
}
},
{
"scope": [
"entity.name.tag.css",
"entity.name.tag.less"
],
"settings": {
"foreground": "#d7ba7d"
}
},
{
"scope": "entity.other.attribute-name",
"settings": {
"foreground": "#9cdcfe"
}
},
{
"scope": [
"entity.other.attribute-name.class.css",
"source.css entity.other.attribute-name.class",
"entity.other.attribute-name.id.css",
"entity.other.attribute-name.parent-selector.css",
"entity.other.attribute-name.parent.less",
"source.css entity.other.attribute-name.pseudo-class",
"entity.other.attribute-name.pseudo-element.css",
"source.css.less entity.other.attribute-name.id",
"entity.other.attribute-name.scss"
],
"settings": {
"foreground": "#d7ba7d"
}
},
{
"scope": "invalid",
"settings": {
"foreground": "#f44747"
}
},
{
"scope": "markup.underline",
"settings": {
"fontStyle": "underline"
}
},
{
"scope": "markup.bold",
"settings": {
"fontStyle": "bold"
}
},
{
"scope": "markup.heading",
"settings": {
"fontStyle": "bold",
"foreground": "#6796e6"
}
},
{
"scope": "markup.italic",
"settings": {
"fontStyle": "italic"
}
},
{
"scope": "markup.strikethrough",
"settings": {
"fontStyle": "strikethrough"
}
},
{
"scope": "markup.inserted",
"settings": {
"foreground": "#b5cea8"
}
},
{
"scope": "markup.deleted",
"settings": {
"foreground": "#ce9178"
}
},
{
"scope": "markup.changed",
"settings": {
"foreground": "#569cd6"
}
},
{
"name": "brackets of XML/HTML tags",
"scope": [
"punctuation.definition.tag"
],
"settings": {
"foreground": "#808080"
}
},
{
"scope": "meta.preprocessor",
"settings": {
"foreground": "#569cd6"
}
},
{
"scope": "meta.preprocessor.string",
"settings": {
"foreground": "#ce9178"
}
},
{
"scope": "meta.preprocessor.numeric",
"settings": {
"foreground": "#b5cea8"
}
},
{
"scope": "meta.structure.dictionary.key.python",
"settings": {
"foreground": "#9cdcfe"
}
},
{
"scope": "storage",
"settings": {
"foreground": "#569cd6"
}
},
{
"scope": "storage.type",
"settings": {
"foreground": "#569cd6"
}
},
{
"scope": "storage.modifier",
"settings": {
"foreground": "#569cd6"
}
},
{
"scope": "string",
"settings": {
"foreground": "#ce9178"
}
},
{
"scope": "string.tag",
"settings": {
"foreground": "#ce9178"
}
},
{
"scope": "string.value",
"settings": {
"foreground": "#ce9178"
}
},
{
"scope": "string.regexp",
"settings": {
"foreground": "#d16969"
}
},
{
"name": "String interpolation",
"scope": [
"punctuation.definition.template-expression.begin",
"punctuation.definition.template-expression.end",
"punctuation.section.embedded"
],
"settings": {
"foreground": "#569cd6"
}
},
{
"name": "Reset JavaScript string interpolation expression",
"scope": [
"meta.template.expression"
],
"settings": {
"foreground": "#ffffff"
}
},
{
"scope": [
"support.type.vendored.property-name",
"support.type.property-name",
"source.css variable",
"source.coffee.embedded"
],
"settings": {
"foreground": "#d4d4d4"
}
},
{
"scope": "keyword",
"settings": {
"foreground": "#569cd6"
}
},
{
"scope": "keyword.control",
"settings": {
"foreground": "#569cd6"
}
},
{
"scope": "keyword.operator",
"settings": {
"foreground": "#d4d4d4"
}
},
{
"scope": [
"keyword.operator.new",
"keyword.operator.expression",
"keyword.operator.cast",
"keyword.operator.sizeof",
"keyword.operator.logical.python"
],
"settings": {
"foreground": "#569cd6"
}
},
{
"scope": "keyword.other.unit",
"settings": {
"foreground": "#b5cea8"
}
},
{
"scope": "support.function.git-rebase",
"settings": {
"foreground": "#d4d4d4"
}
},
{
"scope": "constant.sha.git-rebase",
"settings": {
"foreground": "#b5cea8"
}
},
{
"name": "coloring of the Java import and package identifiers",
"scope": [
"storage.modifier.import.java",
"variable.language.wildcard.java",
"storage.modifier.package.java"
],
"settings": {
"foreground": "#d4d4d4"
}
},
{
"name": "coloring of the TS this",
"scope": "variable.language.this",
"settings": {
"foreground": "#569cd6"
}
},
{
"name": "Function declarations",
"scope": [
"entity.name.function",
"support.function",
"support.constant.handlebars",
"source.powershell variable.other.member"
],
"settings": {
"foreground": "#DCDCAA"
}
},
{
"name": "Types declaration and references",
"scope": [
"support.class",
"support.type",
"entity.name.type",
"entity.name.namespace",
"entity.name.scope-resolution",
"entity.name.class",
"storage.type.cs",
"storage.type.generic.cs",
"storage.type.modifier.cs",
"storage.type.variable.cs",
"storage.type.annotation.java",
"storage.type.generic.java",
"storage.type.java",
"storage.type.object.array.java",
"storage.type.primitive.array.java",
"storage.type.primitive.java",
"storage.type.token.java",
"storage.type.groovy",
"storage.type.annotation.groovy",
"storage.type.parameters.groovy",
"storage.type.generic.groovy",
"storage.type.object.array.groovy",
"storage.type.primitive.array.groovy",
"storage.type.primitive.groovy"
],
"settings": {
"foreground": "#4EC9B0"
}
},
{
"name": "Types declaration and references, TS grammar specific",
"scope": [
"meta.type.cast.expr",
"meta.type.new.expr",
"support.constant.math",
"support.constant.dom",
"support.constant.json",
"entity.other.inherited-class",
"punctuation.separator.namespace.ruby"
],
"settings": {
"foreground": "#4EC9B0"
}
},
{
"name": "Control flow / Special keywords",
"scope": [
"keyword.control",
"source.cpp keyword.operator.new",
"source.cpp keyword.operator.delete",
"keyword.other.using",
"keyword.other.directive.using",
"keyword.other.operator"
],
"settings": {
"foreground": "#C586C0"
}
},
{
"name": "Variable and parameter name",
"scope": [
"variable",
"meta.definition.variable.name",
"support.variable"
],
"settings": {
"foreground": "#9CDCFE"
}
},
{
"name": "Object keys, TS grammar specific",
"scope": [
"meta.object-literal.key"
],
"settings": {
"foreground": "#9CDCFE"
}
},
{
"name": "CSS property value",
"scope": [
"support.constant.property-value",
"support.constant.font-name",
"support.constant.media-type",
"support.constant.media",
"constant.other.color.rgb-value",
"constant.other.rgb-value",
"support.constant.color"
],
"settings": {
"foreground": "#CE9178"
}
},
{
"name": "HC Search Editor context line override",
"scope": "meta.resultLinePrefix.contextLinePrefix.search",
"settings": {
"foreground": "#CBEDCB"
}
}
],
"semanticHighlighting": true,
"semanticTokenColors": {
"newOperator": "#FFFFFF",
"stringLiteral": "#ce9178",
"customLiteral": "#DCDCAA",
"numberLiteral": "#b5cea8"
}
}

View File

@@ -0,0 +1,597 @@
{
"$schema": "vscode://schemas/color-theme",
"name": "Light High Contrast",
"tokenColors": [
{
"scope": [
"meta.embedded",
"source.groovy.embedded",
"variable.legacy.builtin.python"
],
"settings": {
"foreground": "#292929"
}
},
{
"scope": "emphasis",
"settings": {
"fontStyle": "italic"
}
},
{
"scope": "strong",
"settings": {
"fontStyle": "bold"
}
},
{
"scope": "meta.diff.header",
"settings": {
"foreground": "#062F4A"
}
},
{
"scope": "comment",
"settings": {
"foreground": "#515151"
}
},
{
"scope": "constant.language",
"settings": {
"foreground": "#0F4A85"
}
},
{
"scope": [
"constant.numeric",
"variable.other.enummember",
"keyword.operator.plus.exponent",
"keyword.operator.minus.exponent"
],
"settings": {
"foreground": "#096d48"
}
},
{
"scope": "constant.regexp",
"settings": {
"foreground": "#811F3F"
}
},
{
"scope": "entity.name.tag",
"settings": {
"foreground": "#0F4A85"
}
},
{
"scope": "entity.name.selector",
"settings": {
"foreground": "#0F4A85"
}
},
{
"scope": "entity.other.attribute-name",
"settings": {
"foreground": "#264F78"
}
},
{
"scope": [
"entity.other.attribute-name.class.css",
"source.css entity.other.attribute-name.class",
"entity.other.attribute-name.id.css",
"entity.other.attribute-name.parent-selector.css",
"entity.other.attribute-name.parent.less",
"source.css entity.other.attribute-name.pseudo-class",
"entity.other.attribute-name.pseudo-element.css",
"source.css.less entity.other.attribute-name.id",
"entity.other.attribute-name.scss"
],
"settings": {
"foreground": "#0F4A85"
}
},
{
"scope": "invalid",
"settings": {
"foreground": "#B5200D"
}
},
{
"scope": "markup.underline",
"settings": {
"fontStyle": "underline"
}
},
{
"scope": "markup.bold",
"settings": {
"foreground": "#000080",
"fontStyle": "bold"
}
},
{
"scope": "markup.heading",
"settings": {
"foreground": "#0F4A85",
"fontStyle": "bold"
}
},
{
"scope": "markup.italic",
"settings": {
"fontStyle": "italic"
}
},
{
"scope": "markup.strikethrough",
"settings": {
"fontStyle": "strikethrough"
}
},
{
"scope": "markup.inserted",
"settings": {
"foreground": "#096d48"
}
},
{
"scope": "markup.deleted",
"settings": {
"foreground": "#5A5A5A"
}
},
{
"scope": "markup.changed",
"settings": {
"foreground": "#0451A5"
}
},
{
"scope": [
"punctuation.definition.quote.begin.markdown",
"punctuation.definition.list.begin.markdown"
],
"settings": {
"foreground": "#0451A5"
}
},
{
"scope": "markup.inline.raw",
"settings": {
"foreground": "#0F4A85"
}
},
{
"scope": "punctuation.definition.tag",
"settings": {
"foreground": "#0F4A85"
}
},
{
"scope": [
"meta.preprocessor",
"entity.name.function.preprocessor"
],
"settings": {
"foreground": "#0F4A85"
}
},
{
"scope": "meta.preprocessor.string",
"settings": {
"foreground": "#b5200d"
}
},
{
"scope": "meta.preprocessor.numeric",
"settings": {
"foreground": "#096d48"
}
},
{
"scope": "meta.structure.dictionary.key.python",
"settings": {
"foreground": "#0451A5"
}
},
{
"scope": "storage",
"settings": {
"foreground": "#0F4A85"
}
},
{
"scope": "storage.type",
"settings": {
"foreground": "#0F4A85"
}
},
{
"scope": [
"storage.modifier",
"keyword.operator.noexcept"
],
"settings": {
"foreground": "#0F4A85"
}
},
{
"scope": [
"string",
"meta.embedded.assembly"
],
"settings": {
"foreground": "#0F4A85"
}
},
{
"scope": [
"string.comment.buffered.block.pug",
"string.quoted.pug",
"string.interpolated.pug",
"string.unquoted.plain.in.yaml",
"string.unquoted.plain.out.yaml",
"string.unquoted.block.yaml",
"string.quoted.single.yaml",
"string.quoted.double.xml",
"string.quoted.single.xml",
"string.unquoted.cdata.xml",
"string.quoted.double.html",
"string.quoted.single.html",
"string.unquoted.html",
"string.quoted.single.handlebars",
"string.quoted.double.handlebars"
],
"settings": {
"foreground": "#0F4A85"
}
},
{
"scope": "string.regexp",
"settings": {
"foreground": "#811F3F"
}
},
{
"scope": [
"punctuation.definition.template-expression.begin",
"punctuation.definition.template-expression.end",
"punctuation.section.embedded"
],
"settings": {
"foreground": "#0F4A85"
}
},
{
"scope": [
"meta.template.expression"
],
"settings": {
"foreground": "#000000"
}
},
{
"scope": [
"support.constant.property-value",
"support.constant.font-name",
"support.constant.media-type",
"support.constant.media",
"constant.other.color.rgb-value",
"constant.other.rgb-value",
"support.constant.color"
],
"settings": {
"foreground": "#0451A5"
}
},
{
"scope": [
"support.type.vendored.property-name",
"support.type.property-name",
"source.css variable",
"source.coffee.embedded"
],
"settings": {
"foreground": "#264F78"
}
},
{
"scope": [
"support.type.property-name.json"
],
"settings": {
"foreground": "#0451A5"
}
},
{
"scope": "keyword",
"settings": {
"foreground": "#0F4A85"
}
},
{
"scope": "keyword.control",
"settings": {
"foreground": "#0F4A85"
}
},
{
"scope": "keyword.operator",
"settings": {
"foreground": "#000000"
}
},
{
"scope": [
"keyword.operator.new",
"keyword.operator.expression",
"keyword.operator.cast",
"keyword.operator.sizeof",
"keyword.operator.alignof",
"keyword.operator.typeid",
"keyword.operator.alignas",
"keyword.operator.instanceof",
"keyword.operator.logical.python",
"keyword.operator.wordlike"
],
"settings": {
"foreground": "#0F4A85"
}
},
{
"scope": "keyword.other.unit",
"settings": {
"foreground": "#096d48"
}
},
{
"scope": [
"punctuation.section.embedded.begin.php",
"punctuation.section.embedded.end.php"
],
"settings": {
"foreground": "#0F4A85"
}
},
{
"scope": "support.function.git-rebase",
"settings": {
"foreground": "#0451A5"
}
},
{
"scope": "constant.sha.git-rebase",
"settings": {
"foreground": "#096d48"
}
},
{
"scope": [
"storage.modifier.import.java",
"variable.language.wildcard.java",
"storage.modifier.package.java"
],
"settings": {
"foreground": "#000000"
}
},
{
"scope": "variable.language",
"settings": {
"foreground": "#0F4A85"
}
},
{
"scope": [
"entity.name.function",
"support.function",
"support.constant.handlebars",
"source.powershell variable.other.member",
"entity.name.operator.custom-literal"
],
"settings": {
"foreground": "#5e2cbc"
}
},
{
"scope": [
"support.class",
"support.type",
"entity.name.type",
"entity.name.namespace",
"entity.other.attribute",
"entity.name.scope-resolution",
"entity.name.class",
"storage.type.numeric.go",
"storage.type.byte.go",
"storage.type.boolean.go",
"storage.type.string.go",
"storage.type.uintptr.go",
"storage.type.error.go",
"storage.type.rune.go",
"storage.type.cs",
"storage.type.generic.cs",
"storage.type.modifier.cs",
"storage.type.variable.cs",
"storage.type.annotation.java",
"storage.type.generic.java",
"storage.type.java",
"storage.type.object.array.java",
"storage.type.primitive.array.java",
"storage.type.primitive.java",
"storage.type.token.java",
"storage.type.groovy",
"storage.type.annotation.groovy",
"storage.type.parameters.groovy",
"storage.type.generic.groovy",
"storage.type.object.array.groovy",
"storage.type.primitive.array.groovy",
"storage.type.primitive.groovy"
],
"settings": {
"foreground": "#185E73"
}
},
{
"scope": [
"meta.type.cast.expr",
"meta.type.new.expr",
"support.constant.math",
"support.constant.dom",
"support.constant.json",
"entity.other.inherited-class",
"punctuation.separator.namespace.ruby"
],
"settings": {
"foreground": "#185E73"
}
},
{
"scope": [
"keyword.control",
"source.cpp keyword.operator.new",
"source.cpp keyword.operator.delete",
"keyword.other.using",
"keyword.other.directive.using",
"keyword.other.operator",
"entity.name.operator"
],
"settings": {
"foreground": "#b5200d"
}
},
{
"scope": [
"variable",
"meta.definition.variable.name",
"support.variable",
"entity.name.variable",
"constant.other.placeholder"
],
"settings": {
"foreground": "#001080"
}
},
{
"scope": [
"variable.other.constant",
"variable.other.enummember"
],
"settings": {
"foreground": "#02715D"
}
},
{
"scope": [
"meta.object-literal.key"
],
"settings": {
"foreground": "#001080"
}
},
{
"scope": [
"support.constant.property-value",
"support.constant.font-name",
"support.constant.media-type",
"support.constant.media",
"constant.other.color.rgb-value",
"constant.other.rgb-value",
"support.constant.color"
],
"settings": {
"foreground": "#0451A5"
}
},
{
"scope": [
"punctuation.definition.group.regexp",
"punctuation.definition.group.assertion.regexp",
"punctuation.definition.character-class.regexp",
"punctuation.character.set.begin.regexp",
"punctuation.character.set.end.regexp",
"keyword.operator.negation.regexp",
"support.other.parenthesis.regexp"
],
"settings": {
"foreground": "#D16969"
}
},
{
"scope": [
"constant.character.character-class.regexp",
"constant.other.character-class.set.regexp",
"constant.other.character-class.regexp",
"constant.character.set.regexp"
],
"settings": {
"foreground": "#811F3F"
}
},
{
"scope": "keyword.operator.quantifier.regexp",
"settings": {
"foreground": "#000000"
}
},
{
"scope": [
"keyword.operator.or.regexp",
"keyword.control.anchor.regexp"
],
"settings": {
"foreground": "#EE0000"
}
},
{
"scope": "constant.character",
"settings": {
"foreground": "#0F4A85"
}
},
{
"scope": "constant.character.escape",
"settings": {
"foreground": "#EE0000"
}
},
{
"scope": "entity.name.label",
"settings": {
"foreground": "#000000"
}
},
{
"scope": "token.info-token",
"settings": {
"foreground": "#316BCD"
}
},
{
"scope": "token.warn-token",
"settings": {
"foreground": "#CD9731"
}
},
{
"scope": "token.error-token",
"settings": {
"foreground": "#CD3131"
}
},
{
"scope": "token.debug-token",
"settings": {
"foreground": "#800080"
}
}
],
"colors": {
"actionBar.toggledBackground": "#dddddd",
"statusBarItem.remoteBackground": "#FFFFFF",
"statusBarItem.remoteForeground": "#000000"
}
}

View File

@@ -0,0 +1,5 @@
{
"$schema": "vscode://schemas/color-theme",
"name": "Dark High Contrast (Theia)",
"include": "./hc_black.json"
}

View File

@@ -0,0 +1,5 @@
{
"$schema": "vscode://schemas/color-theme",
"name": "Light High Contrast (Theia)",
"include": "./hc_light.json"
}

View File

@@ -0,0 +1,206 @@
{
"$schema": "vscode://schemas/color-theme",
"name": "Light+",
"include": "./light_vs.json",
"tokenColors": [
{
"name": "Function declarations",
"scope": [
"entity.name.function",
"support.function",
"support.constant.handlebars",
"source.powershell variable.other.member",
"entity.name.operator.custom-literal"
],
"settings": {
"foreground": "#795E26"
}
},
{
"name": "Types declaration and references",
"scope": [
"support.class",
"support.type",
"entity.name.type",
"entity.name.namespace",
"entity.other.attribute",
"entity.name.scope-resolution",
"entity.name.class",
"storage.type.numeric.go",
"storage.type.byte.go",
"storage.type.boolean.go",
"storage.type.string.go",
"storage.type.uintptr.go",
"storage.type.error.go",
"storage.type.rune.go",
"storage.type.cs",
"storage.type.generic.cs",
"storage.type.modifier.cs",
"storage.type.variable.cs",
"storage.type.annotation.java",
"storage.type.generic.java",
"storage.type.java",
"storage.type.object.array.java",
"storage.type.primitive.array.java",
"storage.type.primitive.java",
"storage.type.token.java",
"storage.type.groovy",
"storage.type.annotation.groovy",
"storage.type.parameters.groovy",
"storage.type.generic.groovy",
"storage.type.object.array.groovy",
"storage.type.primitive.array.groovy",
"storage.type.primitive.groovy"
],
"settings": {
"foreground": "#267f99"
}
},
{
"name": "Types declaration and references, TS grammar specific",
"scope": [
"meta.type.cast.expr",
"meta.type.new.expr",
"support.constant.math",
"support.constant.dom",
"support.constant.json",
"entity.other.inherited-class",
"punctuation.separator.namespace.ruby"
],
"settings": {
"foreground": "#267f99"
}
},
{
"name": "Control flow / Special keywords",
"scope": [
"keyword.control",
"source.cpp keyword.operator.new",
"source.cpp keyword.operator.delete",
"keyword.other.using",
"keyword.other.directive.using",
"keyword.other.operator",
"entity.name.operator"
],
"settings": {
"foreground": "#AF00DB"
}
},
{
"name": "Variable and parameter name",
"scope": [
"variable",
"meta.definition.variable.name",
"support.variable",
"entity.name.variable",
"constant.other.placeholder"
],
"settings": {
"foreground": "#001080"
}
},
{
"name": "Constants and enums",
"scope": [
"variable.other.constant",
"variable.other.enummember"
],
"settings": {
"foreground": "#0070C1"
}
},
{
"name": "Object keys, TS grammar specific",
"scope": [
"meta.object-literal.key"
],
"settings": {
"foreground": "#001080"
}
},
{
"name": "CSS property value",
"scope": [
"support.constant.property-value",
"support.constant.font-name",
"support.constant.media-type",
"support.constant.media",
"constant.other.color.rgb-value",
"constant.other.rgb-value",
"support.constant.color"
],
"settings": {
"foreground": "#0451a5"
}
},
{
"name": "Regular expression groups",
"scope": [
"punctuation.definition.group.regexp",
"punctuation.definition.group.assertion.regexp",
"punctuation.definition.character-class.regexp",
"punctuation.character.set.begin.regexp",
"punctuation.character.set.end.regexp",
"keyword.operator.negation.regexp",
"support.other.parenthesis.regexp"
],
"settings": {
"foreground": "#d16969"
}
},
{
"scope": [
"constant.character.character-class.regexp",
"constant.other.character-class.set.regexp",
"constant.other.character-class.regexp",
"constant.character.set.regexp"
],
"settings": {
"foreground": "#811f3f"
}
},
{
"scope": "keyword.operator.quantifier.regexp",
"settings": {
"foreground": "#000000"
}
},
{
"scope": [
"keyword.operator.or.regexp",
"keyword.control.anchor.regexp"
],
"settings": {
"foreground": "#EE0000"
}
},
{
"scope": [
"constant.character",
"constant.other.option"
],
"settings": {
"foreground": "#0000ff"
}
},
{
"scope": "constant.character.escape",
"settings": {
"foreground": "#EE0000"
}
},
{
"scope": "entity.name.label",
"settings": {
"foreground": "#000000"
}
}
],
"semanticHighlighting": true,
"semanticTokenColors": {
"newOperator": "#AF00DB",
"stringLiteral": "#a31515",
"customLiteral": "#795E26",
"numberLiteral": "#098658"
}
}

View File

@@ -0,0 +1,10 @@
{
"$schema": "vscode://schemas/color-theme",
"name": "Light (Theia)",
"include": "./light_plus.json",
"colors": {
"activityBar.background": "#ececec",
"activityBar.activeBorder": "#000000",
"activityBar.foreground": "#000000"
}
}

View File

@@ -0,0 +1,437 @@
{
"$schema": "vscode://schemas/color-theme",
"name": "Light (Visual Studio)",
"colors": {
"checkbox.border": "#919191",
"editor.background": "#FFFFFF",
"editor.foreground": "#000000",
"editor.inactiveSelectionBackground": "#E5EBF1",
"editorIndentGuide.background1": "#D3D3D3",
"editorIndentGuide.activeBackground1": "#939393",
"editor.selectionHighlightBackground": "#ADD6FF80",
"editorSuggestWidget.background": "#F3F3F3",
"activityBarBadge.background": "#007ACC",
"sideBarTitle.foreground": "#6F6F6F",
"list.hoverBackground": "#E8E8E8",
"menu.border": "#D4D4D4",
"input.placeholderForeground": "#767676",
"searchEditor.textInputBorder": "#CECECE",
"settings.textInputBorder": "#CECECE",
"settings.numberInputBorder": "#CECECE",
"statusBarItem.remoteForeground": "#FFF",
"statusBarItem.remoteBackground": "#16825D",
"ports.iconRunningProcessForeground": "#369432",
"sideBarSectionHeader.background": "#0000",
"sideBarSectionHeader.border": "#61616130",
"tab.selectedForeground": "#333333b3",
"tab.selectedBackground": "#ffffffa5",
"tab.lastPinnedBorder": "#61616130",
"notebook.cellBorderColor": "#E8E8E8",
"notebook.selectedCellBackground": "#c8ddf150",
"statusBarItem.errorBackground": "#c72e0f",
"list.activeSelectionIconForeground": "#FFF",
"list.focusAndSelectionOutline": "#90C2F9",
"terminal.inactiveSelectionBackground": "#E5EBF1",
"widget.border": "#d4d4d4",
"actionBar.toggledBackground": "#dddddd",
"diffEditor.unchangedRegionBackground": "#f8f8f8"
},
"tokenColors": [
{
"scope": [
"meta.embedded",
"source.groovy.embedded",
"string meta.image.inline.markdown",
"variable.legacy.builtin.python"
],
"settings": {
"foreground": "#000000ff"
}
},
{
"scope": "emphasis",
"settings": {
"fontStyle": "italic"
}
},
{
"scope": "strong",
"settings": {
"fontStyle": "bold"
}
},
{
"scope": "meta.diff.header",
"settings": {
"foreground": "#000080"
}
},
{
"scope": "comment",
"settings": {
"foreground": "#008000"
}
},
{
"scope": "constant.language",
"settings": {
"foreground": "#0000ff"
}
},
{
"scope": [
"constant.numeric",
"variable.other.enummember",
"keyword.operator.plus.exponent",
"keyword.operator.minus.exponent"
],
"settings": {
"foreground": "#098658"
}
},
{
"scope": "constant.regexp",
"settings": {
"foreground": "#811f3f"
}
},
{
"name": "css tags in selectors, xml tags",
"scope": "entity.name.tag",
"settings": {
"foreground": "#800000"
}
},
{
"scope": "entity.name.selector",
"settings": {
"foreground": "#800000"
}
},
{
"scope": "entity.other.attribute-name",
"settings": {
"foreground": "#e50000"
}
},
{
"scope": [
"entity.other.attribute-name.class.css",
"source.css entity.other.attribute-name.class",
"entity.other.attribute-name.id.css",
"entity.other.attribute-name.parent-selector.css",
"entity.other.attribute-name.parent.less",
"source.css entity.other.attribute-name.pseudo-class",
"entity.other.attribute-name.pseudo-element.css",
"source.css.less entity.other.attribute-name.id",
"entity.other.attribute-name.scss"
],
"settings": {
"foreground": "#800000"
}
},
{
"scope": "invalid",
"settings": {
"foreground": "#cd3131"
}
},
{
"scope": "markup.underline",
"settings": {
"fontStyle": "underline"
}
},
{
"scope": "markup.bold",
"settings": {
"fontStyle": "bold",
"foreground": "#000080"
}
},
{
"scope": "markup.heading",
"settings": {
"fontStyle": "bold",
"foreground": "#800000"
}
},
{
"scope": "markup.italic",
"settings": {
"fontStyle": "italic"
}
},
{
"scope": "markup.strikethrough",
"settings": {
"fontStyle": "strikethrough"
}
},
{
"scope": "markup.inserted",
"settings": {
"foreground": "#098658"
}
},
{
"scope": "markup.deleted",
"settings": {
"foreground": "#a31515"
}
},
{
"scope": "markup.changed",
"settings": {
"foreground": "#0451a5"
}
},
{
"scope": [
"punctuation.definition.quote.begin.markdown",
"punctuation.definition.list.begin.markdown"
],
"settings": {
"foreground": "#0451a5"
}
},
{
"scope": "markup.inline.raw",
"settings": {
"foreground": "#800000"
}
},
{
"name": "brackets of XML/HTML tags",
"scope": "punctuation.definition.tag",
"settings": {
"foreground": "#800000"
}
},
{
"scope": [
"meta.preprocessor",
"entity.name.function.preprocessor"
],
"settings": {
"foreground": "#0000ff"
}
},
{
"scope": "meta.preprocessor.string",
"settings": {
"foreground": "#a31515"
}
},
{
"scope": "meta.preprocessor.numeric",
"settings": {
"foreground": "#098658"
}
},
{
"scope": "meta.structure.dictionary.key.python",
"settings": {
"foreground": "#0451a5"
}
},
{
"scope": "storage",
"settings": {
"foreground": "#0000ff"
}
},
{
"scope": "storage.type",
"settings": {
"foreground": "#0000ff"
}
},
{
"scope": [
"storage.modifier",
"keyword.operator.noexcept"
],
"settings": {
"foreground": "#0000ff"
}
},
{
"scope": [
"string",
"meta.embedded.assembly"
],
"settings": {
"foreground": "#a31515"
}
},
{
"scope": [
"string.comment.buffered.block.pug",
"string.quoted.pug",
"string.interpolated.pug",
"string.unquoted.plain.in.yaml",
"string.unquoted.plain.out.yaml",
"string.unquoted.block.yaml",
"string.quoted.single.yaml",
"string.quoted.double.xml",
"string.quoted.single.xml",
"string.unquoted.cdata.xml",
"string.quoted.double.html",
"string.quoted.single.html",
"string.unquoted.html",
"string.quoted.single.handlebars",
"string.quoted.double.handlebars"
],
"settings": {
"foreground": "#0000ff"
}
},
{
"scope": "string.regexp",
"settings": {
"foreground": "#811f3f"
}
},
{
"name": "String interpolation",
"scope": [
"punctuation.definition.template-expression.begin",
"punctuation.definition.template-expression.end",
"punctuation.section.embedded"
],
"settings": {
"foreground": "#0000ff"
}
},
{
"name": "Reset JavaScript string interpolation expression",
"scope": [
"meta.template.expression"
],
"settings": {
"foreground": "#000000"
}
},
{
"scope": [
"support.constant.property-value",
"support.constant.font-name",
"support.constant.media-type",
"support.constant.media",
"constant.other.color.rgb-value",
"constant.other.rgb-value",
"support.constant.color"
],
"settings": {
"foreground": "#0451a5"
}
},
{
"scope": [
"support.type.vendored.property-name",
"support.type.property-name",
"source.css variable",
"source.coffee.embedded"
],
"settings": {
"foreground": "#e50000"
}
},
{
"scope": [
"support.type.property-name.json"
],
"settings": {
"foreground": "#0451a5"
}
},
{
"scope": "keyword",
"settings": {
"foreground": "#0000ff"
}
},
{
"scope": "keyword.control",
"settings": {
"foreground": "#0000ff"
}
},
{
"scope": "keyword.operator",
"settings": {
"foreground": "#000000"
}
},
{
"scope": [
"keyword.operator.new",
"keyword.operator.expression",
"keyword.operator.cast",
"keyword.operator.sizeof",
"keyword.operator.alignof",
"keyword.operator.typeid",
"keyword.operator.alignas",
"keyword.operator.instanceof",
"keyword.operator.logical.python",
"keyword.operator.wordlike"
],
"settings": {
"foreground": "#0000ff"
}
},
{
"scope": "keyword.other.unit",
"settings": {
"foreground": "#098658"
}
},
{
"scope": [
"punctuation.section.embedded.begin.php",
"punctuation.section.embedded.end.php"
],
"settings": {
"foreground": "#800000"
}
},
{
"scope": "support.function.git-rebase",
"settings": {
"foreground": "#0451a5"
}
},
{
"scope": "constant.sha.git-rebase",
"settings": {
"foreground": "#098658"
}
},
{
"name": "coloring of the Java import and package identifiers",
"scope": [
"storage.modifier.import.java",
"variable.language.wildcard.java",
"storage.modifier.package.java"
],
"settings": {
"foreground": "#000000"
}
},
{
"name": "this.self",
"scope": "variable.language",
"settings": {
"foreground": "#0000ff"
}
}
],
"semanticHighlighting": true,
"semanticTokenColors": {
"newOperator": "#0000ff",
"stringLiteral": "#a31515",
"customLiteral": "#000000",
"numberLiteral": "#098658"
}
}

View File

@@ -0,0 +1,61 @@
{
"name": "@theia/monaco",
"version": "1.68.0",
"description": "Theia - Monaco Extension",
"dependencies": {
"@theia/core": "1.68.0",
"@theia/editor": "1.68.0",
"@theia/filesystem": "1.68.0",
"@theia/markers": "1.68.0",
"@theia/monaco-editor-core": "1.96.302",
"@theia/outline-view": "1.68.0",
"@theia/workspace": "1.68.0",
"fast-plist": "^0.1.2",
"idb": "^4.0.5",
"jsonc-parser": "^2.2.0",
"tslib": "^2.6.2",
"vscode-oniguruma": "2.0.1",
"vscode-textmate": "^9.2.0"
},
"publishConfig": {
"access": "public"
},
"theiaExtensions": [
{
"frontend": "lib/browser/monaco-frontend-module",
"secondaryWindow": "lib/browser/monaco-frontend-module"
}
],
"keywords": [
"theia-extension"
],
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
"repository": {
"type": "git",
"url": "https://github.com/eclipse-theia/theia.git"
},
"bugs": {
"url": "https://github.com/eclipse-theia/theia/issues"
},
"homepage": "https://github.com/eclipse-theia/theia",
"files": [
"lib",
"src",
"data"
],
"scripts": {
"build": "theiaext build",
"clean": "theiaext clean",
"compile": "theiaext compile",
"lint": "theiaext lint",
"test": "theiaext test",
"watch": "theiaext watch"
},
"devDependencies": {
"@theia/ext-scripts": "1.68.0"
},
"nyc": {
"extends": "../../configs/nyc.json"
},
"gitHead": "21358137e41342742707f660b8e222f940a27652"
}

View File

@@ -0,0 +1,74 @@
// *****************************************************************************
// Copyright (C) 2025 and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { IPosition } from '@theia/monaco-editor-core/esm/vs/editor/common/core/position';
import { ContentHoverWidget } from '@theia/monaco-editor-core/esm/vs/editor/contrib/hover/browser/contentHoverWidget';
// https://github.com/microsoft/vscode/blob/1430e1845cbf5ec29a2fc265f12c7fb5c3d685c3/src/vs/editor/contrib/hover/browser/resizableContentWidget.ts#L13-L14
const VSCODE_TOP_HEIGHT = 30;
const VSCODE_BOTTOM_HEIGHT = 24;
export interface SetActualHeightForContentHoverWidgetParams {
topHeight?: number;
bottomHeight?: number;
}
export interface ContentHoverWidgetPatcher {
setActualHeightForContentHoverWidget(params: SetActualHeightForContentHoverWidgetParams): void;
}
export function createContentHoverWidgetPatcher(): ContentHoverWidgetPatcher {
let actualTopDiff: number | undefined;
let actualBottomDiff: number | undefined;
const originalAvailableVerticalSpaceAbove = ContentHoverWidget.prototype['_availableVerticalSpaceAbove'];
ContentHoverWidget.prototype['_availableVerticalSpaceAbove'] = function (position: IPosition): number | undefined {
const originalValue = originalAvailableVerticalSpaceAbove.call(this, position);
if (typeof originalValue !== 'number' || !actualTopDiff) {
return originalValue;
}
// The original implementation deducts the height of the top panel from the total available space.
// https://github.com/microsoft/vscode/blob/1430e1845cbf5ec29a2fc265f12c7fb5c3d685c3/src/vs/editor/contrib/hover/browser/resizableContentWidget.ts#L71
// However, in Theia, the top panel has generally different size (especially when the toolbar is visible).
// This additional height must be further subtracted from the computed height for accurate positioning.
return originalValue - actualTopDiff;
};
const originalAvailableVerticalSpaceBelow = ContentHoverWidget.prototype['_availableVerticalSpaceBelow'];
ContentHoverWidget.prototype['_availableVerticalSpaceBelow'] = function (position: IPosition): number | undefined {
const originalValue = originalAvailableVerticalSpaceBelow.call(this, position);
if (typeof originalValue !== 'number' || !actualBottomDiff) {
return originalValue;
}
// The original method subtracts the height of the bottom panel from the overall available height.
// https://github.com/microsoft/vscode/blob/1430e1845cbf5ec29a2fc265f12c7fb5c3d685c3/src/vs/editor/contrib/hover/browser/resizableContentWidget.ts#L83
// In Theia, the status bar has different height than in VS Code, which means this difference
// should be also removed to ensure the calculated available space is accurate.
// Note that removing negative value will increase the available space.
return originalValue - actualBottomDiff;
};
return {
setActualHeightForContentHoverWidget(params): void {
if (typeof params.topHeight === 'number') {
actualTopDiff = params.topHeight - VSCODE_TOP_HEIGHT;
}
if (typeof params.bottomHeight === 'number') {
actualBottomDiff = params.bottomHeight - VSCODE_BOTTOM_HEIGHT;
}
},
};
}

View File

@@ -0,0 +1,50 @@
// *****************************************************************************
// Copyright (C) 2025 and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { ApplicationShell, FrontendApplication, FrontendApplicationContribution } from '@theia/core/lib/browser';
import { SetActualHeightForContentHoverWidgetParams } from './content-hover-widget-patcher';
import { contentHoverWidgetPatcher } from './monaco-init';
@injectable()
export class DefaultContentHoverWidgetPatcher implements FrontendApplicationContribution {
onStart(app: FrontendApplication): void {
const shell = app.shell;
this.updateContentHoverWidgetHeight({
topHeight: this.getTopPanelHeight(shell),
bottomHeight: this.getStatusBarHeight(shell)
});
shell['statusBar'].onDidChangeVisibility(() => {
this.updateContentHoverWidgetHeight({
bottomHeight: this.getStatusBarHeight(shell)
});
});
}
protected updateContentHoverWidgetHeight(params: SetActualHeightForContentHoverWidgetParams): void {
contentHoverWidgetPatcher.setActualHeightForContentHoverWidget(params);
}
protected getTopPanelHeight(shell: ApplicationShell): number {
return shell.topPanel.node.getBoundingClientRect().height;
}
protected getStatusBarHeight(shell: ApplicationShell): number {
return shell['statusBar'].node.getBoundingClientRect().height;
}
}

View File

@@ -0,0 +1,18 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
export * from './monaco-frontend-module';
export * from './monaco-code-action-service';

View File

@@ -0,0 +1,110 @@
// *****************************************************************************
// Copyright (C) 2022 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { ILanguageService } from '@theia/monaco-editor-core/esm/vs/editor/common/languages/language';
import { MarkdownRenderer as CodeMarkdownRenderer, IMarkdownRendererOptions }
from '@theia/monaco-editor-core/esm/vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
import * as monaco from '@theia/monaco-editor-core';
import { OpenerService, WidgetOpenerOptions, open } from '@theia/core/lib/browser';
import { IOpenerService, OpenExternalOptions, OpenInternalOptions } from '@theia/monaco-editor-core/esm/vs/platform/opener/common/opener';
import { HttpOpenHandlerOptions } from '@theia/core/lib/browser/http-open-handler';
import { URI } from '@theia/core/lib/common/uri';
import { MarkdownRenderer, MarkdownRenderOptions, MarkdownRenderResult } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer';
import { MarkedOptions, MarkdownRenderOptions as MonacoMarkdownRenderOptions } from '@theia/monaco-editor-core/esm/vs/base/browser/markdownRenderer';
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
import { DisposableStore } from '@theia/monaco-editor-core/esm/vs/base/common/lifecycle';
import { DisposableCollection, DisposableGroup, PreferenceService } from '@theia/core';
@injectable()
export class MonacoMarkdownRenderer implements MarkdownRenderer {
@inject(OpenerService) protected readonly openerService: OpenerService;
@inject(PreferenceService) protected readonly preferences: PreferenceService;
protected delegate: CodeMarkdownRenderer;
protected _openerService: OpenerService | undefined;
render(markdown: MarkdownString | undefined, options?: MarkdownRenderOptions, markedOptions?: MarkedOptions): MarkdownRenderResult {
return this.delegate.render(markdown, this.transformOptions(options), markedOptions);
}
protected transformOptions(options?: MarkdownRenderOptions): MonacoMarkdownRenderOptions | undefined {
if (!options?.actionHandler) {
return options as MarkdownRenderOptions & { actionHandler: undefined } | undefined;
}
const monacoActionHandler: MonacoMarkdownRenderOptions['actionHandler'] = {
disposables: this.toDisposableStore(options.actionHandler.disposables),
callback: (content, e) => options.actionHandler!.callback(content, e?.browserEvent)
};
return { ...options, actionHandler: monacoActionHandler };
}
protected toDisposableStore(current: DisposableGroup): DisposableStore {
if (current instanceof DisposableStore) {
return current;
} else if (current instanceof DisposableCollection) {
const store = new DisposableStore();
current['disposables'].forEach(disposable => store.add(disposable));
return store;
} else {
return new DisposableStore();
}
}
@postConstruct()
protected init(): void {
const languages = StandaloneServices.get(ILanguageService);
const openerService = StandaloneServices.get(IOpenerService);
openerService.registerOpener({
open: (u, options) => this.interceptOpen(u, options)
});
const that = this;
const prefs = new class implements IMarkdownRendererOptions {
get codeBlockFontFamily(): string | undefined {
return that.preferences.get<string>('editor.fontFamily');
}
};
this.delegate = new CodeMarkdownRenderer(prefs, languages, openerService);
}
protected async interceptOpen(monacoUri: monaco.Uri | string, monacoOptions?: OpenInternalOptions | OpenExternalOptions): Promise<boolean> {
let options = undefined;
if (monacoOptions) {
if ('openToSide' in monacoOptions && monacoOptions.openToSide) {
options = Object.assign(options || {}, <WidgetOpenerOptions>{
widgetOptions: {
mode: 'split-right'
}
});
}
if ('openExternal' in monacoOptions && monacoOptions.openExternal) {
options = Object.assign(options || {}, <HttpOpenHandlerOptions>{
openExternal: true
});
}
}
const uri = new URI(monacoUri.toString());
try {
await open(this.openerService, uri, options);
return true;
} catch (e) {
console.error(`Fail to open '${uri.toString()}':`, e);
return false;
}
}
}

View File

@@ -0,0 +1,64 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject } from '@theia/core/shared/inversify';
import { MonacoWorkspace } from './monaco-workspace';
import {
IBulkEditOptions, IBulkEditPreviewHandler, IBulkEditResult, IBulkEditService, ResourceEdit
} from '@theia/monaco-editor-core/esm/vs/editor/browser/services/bulkEditService';
import { IDisposable } from '@theia/monaco-editor-core/esm/vs/base/common/lifecycle';
import { WorkspaceEdit } from '@theia/monaco-editor-core/esm/vs/editor/common/languages';
@injectable()
export class MonacoBulkEditService implements IBulkEditService {
declare readonly _serviceBrand: undefined;
@inject(MonacoWorkspace)
protected readonly workspace: MonacoWorkspace;
private _previewHandler?: IBulkEditPreviewHandler;
async apply(editsIn: ResourceEdit[] | WorkspaceEdit, options?: IBulkEditOptions): Promise<IBulkEditResult> {
const edits = Array.isArray(editsIn) ? editsIn : ResourceEdit.convert(editsIn);
if (this._previewHandler && (options?.showPreview || edits.some(value => value.metadata?.needsConfirmation))) {
editsIn = await this._previewHandler(edits, options);
return { ariaSummary: '', isApplied: true };
} else {
return this.workspace.applyBulkEdit(edits, options);
}
}
hasPreviewHandler(): boolean {
return Boolean(this._previewHandler);
}
setPreviewHandler(handler: IBulkEditPreviewHandler): IDisposable {
this._previewHandler = handler;
const disposePreviewHandler = () => {
if (this._previewHandler === handler) {
this._previewHandler = undefined;
}
};
return {
dispose(): void {
disposePreviewHandler();
}
};
}
}

View File

@@ -0,0 +1,49 @@
// *****************************************************************************
// Copyright (C) 2025 STMicroelectronics and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { CancellationToken } from '@theia/core';
import { SaveOptions, SaveReason } from '@theia/core/lib/browser';
import { MonacoEditor } from './monaco-editor';
import { SaveParticipant, SAVE_PARTICIPANT_DEFAULT_ORDER } from './monaco-editor-provider';
import { inject, injectable } from '@theia/core/shared/inversify';
import { MonacoCodeActionService } from './monaco-code-action-service';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// Partially copied from https://github.com/microsoft/vscode/blob/f66e839a38dfe39ee66a86619a790f9c2336d698/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts#L272
@injectable()
export class MonacoCodeActionSaveParticipant implements SaveParticipant {
@inject(MonacoCodeActionService)
protected readonly codeActionService: MonacoCodeActionService;
readonly order = SAVE_PARTICIPANT_DEFAULT_ORDER;
async applyChangesOnSave(editor: MonacoEditor, cancellationToken: CancellationToken, options?: SaveOptions): Promise<void> {
if (options?.saveReason !== SaveReason.Manual) {
return undefined;
}
await this.codeActionService.applyOnSaveCodeActions(
editor.document.textEditorModel,
editor.document.textEditorModel.getLanguageId(),
editor.document.textEditorModel.uri.toString(),
cancellationToken
);
}
}

View File

@@ -0,0 +1,187 @@
// *****************************************************************************
// Copyright (C) 2025 STMicroelectronics and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { CancellationToken } from '@theia/core';
import { inject, injectable } from '@theia/core/shared/inversify';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
import { ILanguageFeaturesService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/languageFeatures';
import { CodeActionKind, CodeActionSet, CodeActionTriggerSource } from '@theia/monaco-editor-core/esm/vs/editor/contrib/codeAction/common/types';
import { applyCodeAction, ApplyCodeActionReason, getCodeActions } from '@theia/monaco-editor-core/esm/vs/editor/contrib/codeAction/browser/codeAction';
import { HierarchicalKind } from '@theia/monaco-editor-core/esm/vs/base/common/hierarchicalKind';
import { EditorPreferences } from '@theia/editor/lib/common/editor-preferences';
import { ITextModel } from '@theia/monaco-editor-core/esm/vs/editor/common/model';
import { CodeActionProvider, CodeActionTriggerType } from '@theia/monaco-editor-core/esm/vs/editor/common/languages';
import { IProgress } from '@theia/monaco-editor-core/esm/vs/platform/progress/common/progress';
import { IInstantiationService } from '@theia/monaco-editor-core/esm/vs/platform/instantiation/common/instantiation';
export const MonacoCodeActionService = Symbol('MonacoCodeActionService');
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// Partially copied from https://github.com/microsoft/vscode/blob/f66e839a38dfe39ee66a86619a790f9c2336d698/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts#L272
export interface MonacoCodeActionService {
/**
* Gets all code actions that should be applied on save for the given model and language identifier.
* @param model The text model to get code actions for
* @param languageId The language identifier for preference lookup
* @param uri The URI string for preference scoping
* @param token Cancellation token
* @returns Array of code action sets to apply, or undefined if no actions should be applied
*/
getAllCodeActionsOnSave(model: ITextModel, languageId: string, uri: string, token: CancellationToken): Promise<CodeActionSet[] | undefined>;
/**
* Applies the provided code actions for the given model.
* @param model The text model to apply code actions to
* @param codeActionSets Array of code action sets to apply
* @param token Cancellation token
*/
applyCodeActions(model: ITextModel, codeActionSets: CodeActionSet[], token: CancellationToken): Promise<void>;
/**
* Applies all code actions that should be run on save for the given model and language identifier.
* This is a convenience method that retrieves all on-save code actions and applies them.
* @param model The text model to apply code actions to
* @param languageId The language identifier for preference lookup
* @param uri The URI string for preference scoping
* @param token Cancellation token
*/
applyOnSaveCodeActions(model: ITextModel, languageId: string, uri: string, token: CancellationToken): Promise<void>;
}
@injectable()
export class MonacoCodeActionServiceImpl implements MonacoCodeActionService {
@inject(EditorPreferences)
protected readonly editorPreferences: EditorPreferences;
async applyOnSaveCodeActions(model: ITextModel, languageId: string, uri: string, token: CancellationToken): Promise<void> {
const codeActionSets = await this.getAllCodeActionsOnSave(model, languageId, uri, token);
if (!codeActionSets || token.isCancellationRequested) {
return;
}
await this.applyCodeActions(model, codeActionSets, token);
}
async getAllCodeActionsOnSave(model: ITextModel, languageId: string, uri: string, token: CancellationToken): Promise<CodeActionSet[] | undefined> {
const setting = this.editorPreferences.get({
preferenceName: 'editor.codeActionsOnSave',
overrideIdentifier: languageId
}, undefined, uri);
if (!setting) {
return undefined;
}
const settingItems: string[] = Array.isArray(setting)
? setting
: Object.keys(setting).filter(x => setting[x]);
const codeActionsOnSave = this.createCodeActionsOnSave(settingItems);
if (!codeActionsOnSave.length) {
return undefined;
}
if (!Array.isArray(setting)) {
codeActionsOnSave.sort((a, b) => {
if (CodeActionKind.SourceFixAll.contains(a)) {
if (CodeActionKind.SourceFixAll.contains(b)) {
return 0;
}
return -1;
}
if (CodeActionKind.SourceFixAll.contains(b)) {
return 1;
}
return 0;
});
}
const excludedActions = Array.isArray(setting)
? []
: Object.keys(setting)
.filter(x => setting[x] === false)
.map(x => new HierarchicalKind(x));
const codeActionSets: CodeActionSet[] = [];
for (const codeActionKind of codeActionsOnSave) {
const actionsToRun = await this.getActionsToRun(model, codeActionKind, excludedActions, token);
if (token.isCancellationRequested) {
actionsToRun.dispose();
break;
}
codeActionSets.push(actionsToRun);
}
return codeActionSets;
}
async applyCodeActions(model: ITextModel, codeActionSets: CodeActionSet[], token: CancellationToken): Promise<void> {
const instantiationService = StandaloneServices.get(IInstantiationService);
for (const codeActionSet of codeActionSets) {
if (token.isCancellationRequested) {
codeActionSet.dispose();
return;
}
try {
for (const action of codeActionSet.validActions) {
await instantiationService.invokeFunction(applyCodeAction, action, ApplyCodeActionReason.OnSave, {}, token);
if (token.isCancellationRequested) {
return;
}
}
} catch {
// Failure to apply a code action should not block other on save actions
} finally {
codeActionSet.dispose();
}
}
}
private createCodeActionsOnSave(settingItems: readonly string[]): HierarchicalKind[] {
const kinds = settingItems.map(x => new HierarchicalKind(x));
// Remove subsets
return kinds.filter(kind => kinds.every(otherKind => otherKind.equals(kind) || !otherKind.contains(kind)));
}
private getActionsToRun(model: ITextModel, codeActionKind: HierarchicalKind, excludes: readonly HierarchicalKind[], token: CancellationToken): Promise<CodeActionSet> {
const { codeActionProvider } = StandaloneServices.get(ILanguageFeaturesService);
const progress: IProgress<CodeActionProvider> = {
report(item): void {
// empty
},
};
return getCodeActions(codeActionProvider, model, model.getFullModelRange(), {
type: CodeActionTriggerType.Auto,
triggerAction: CodeActionTriggerSource.OnSave,
filter: { include: codeActionKind, excludes: excludes, includeSourceActions: true },
}, progress, token);
}
}

View File

@@ -0,0 +1,73 @@
// *****************************************************************************
// Copyright (C) 2019 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
import { Color, ColorDefaults as TheiaColorDefaults, ColorDefinition } from '@theia/core/lib/common/color';
import { Disposable } from '@theia/core/lib/common/disposable';
import { ColorDefaults, ColorValue, getColorRegistry } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/colorRegistry';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
import { IStandaloneThemeService } from '@theia/monaco-editor-core/esm/vs/editor/standalone/common/standaloneTheme';
import { Color as MonacoColor, HSLA, RGBA } from '@theia/monaco-editor-core/esm/vs/base/common/color';
import * as Colors from '@theia/monaco-editor-core/esm/vs/platform/theme/common/colorRegistry';
@injectable()
export class MonacoColorRegistry extends ColorRegistry {
protected readonly monacoThemeService = StandaloneServices.get(IStandaloneThemeService);
protected readonly monacoColorRegistry = getColorRegistry();
override *getColors(): IterableIterator<string> {
for (const { id } of this.monacoColorRegistry.getColors()) {
yield id;
}
}
override getCurrentColor(id: string): string | undefined {
return this.monacoThemeService.getColorTheme().getColor(id)?.toString();
}
getColor(id: string): MonacoColor | undefined {
return this.monacoThemeService.getColorTheme().getColor(id);
}
protected override doRegister(definition: ColorDefinition): Disposable {
const defaults: ColorDefaults = {
dark: this.toColor(TheiaColorDefaults.getDark(definition.defaults)),
light: this.toColor(TheiaColorDefaults.getLight(definition.defaults)),
hcDark: this.toColor(TheiaColorDefaults.getHCDark(definition.defaults)),
hcLight: this.toColor(TheiaColorDefaults.getHCLight(definition.defaults)),
};
const identifier = this.monacoColorRegistry.registerColor(definition.id, defaults, definition.description);
return Disposable.create(() => this.monacoColorRegistry.deregisterColor(identifier));
}
protected toColor(value: Color | undefined): ColorValue | null {
if (!value || typeof value === 'string') {
return value ?? null; // eslint-disable-line no-null/no-null
}
if ('kind' in value) {
return Colors[value.kind](value.v, value.f);
} else if ('r' in value) {
const { r, g, b, a } = value;
return new MonacoColor(new RGBA(r, g, b, a));
} else {
const { h, s, l, a } = value;
return new MonacoColor(new HSLA(h, s, l, a));
}
}
}

View File

@@ -0,0 +1,85 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject } from '@theia/core/shared/inversify';
import { Command, CommandHandler, CommandRegistry, SelectionService } from '@theia/core';
import { TextEditorSelection } from '@theia/editor/lib/browser';
import { MonacoEditor } from './monaco-editor';
import { MonacoEditorProvider } from './monaco-editor-provider';
export interface MonacoEditorCommandHandler {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
execute(editor: MonacoEditor, ...args: any[]): any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
isEnabled?(editor: MonacoEditor, ...args: any[]): boolean;
}
@injectable()
export class MonacoCommandRegistry {
@inject(MonacoEditorProvider)
protected readonly monacoEditors: MonacoEditorProvider;
@inject(CommandRegistry) protected readonly commands: CommandRegistry;
@inject(SelectionService) protected readonly selectionService: SelectionService;
validate(command: string | undefined): string | undefined {
if (!command) {
return undefined;
}
return this.commands.commandIds.indexOf(command) !== -1 ? command : undefined;
}
registerCommand(command: Command, handler: MonacoEditorCommandHandler): void {
this.commands.registerCommand({
...command,
id: command.id
}, this.newHandler(handler));
}
registerHandler(command: string, handler: MonacoEditorCommandHandler): void {
this.commands.registerHandler(command, this.newHandler(handler));
}
protected newHandler(monacoHandler: MonacoEditorCommandHandler): CommandHandler {
return {
execute: (...args) => this.execute(monacoHandler, ...args),
isEnabled: (...args) => this.isEnabled(monacoHandler, ...args),
isVisible: (...args) => this.isVisible(monacoHandler, ...args)
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected execute(monacoHandler: MonacoEditorCommandHandler, ...args: any[]): any {
const editor = this.monacoEditors.current;
if (editor) {
return Promise.resolve(monacoHandler.execute(editor, ...args));
}
return Promise.resolve();
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected isEnabled(monacoHandler: MonacoEditorCommandHandler, ...args: any[]): boolean {
const editor = this.monacoEditors.current;
return !!editor && (!monacoHandler.isEnabled || monacoHandler.isEnabled(editor, ...args));
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected isVisible(monacoHandler: MonacoEditorCommandHandler, ...args: any[]): boolean {
return TextEditorSelection.is(this.selectionService.selection);
}
}

View File

@@ -0,0 +1,90 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { Emitter } from '@theia/core/lib/common/event';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { ICommandEvent, ICommandService } from '@theia/monaco-editor-core/esm/vs/platform/commands/common/commands';
import { StandaloneCommandService, StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
import * as monaco from '@theia/monaco-editor-core';
import { IInstantiationService } from '@theia/monaco-editor-core/esm/vs/platform/instantiation/common/instantiation';
@injectable()
export class MonacoCommandService implements ICommandService, Disposable {
declare readonly _serviceBrand: undefined; // Required for type compliance.
protected readonly onWillExecuteCommandEmitter = new Emitter<ICommandEvent>();
protected readonly onDidExecuteCommandEmitter = new Emitter<ICommandEvent>();
protected readonly toDispose = new DisposableCollection(
this.onWillExecuteCommandEmitter,
this.onDidExecuteCommandEmitter
);
protected delegate: StandaloneCommandService | undefined;
constructor(
@inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry
) {
this.toDispose.push(this.commandRegistry.onWillExecuteCommand(e => this.onWillExecuteCommandEmitter.fire(e)));
this.toDispose.push(this.commandRegistry.onDidExecuteCommand(e => this.onDidExecuteCommandEmitter.fire(e)));
}
@postConstruct()
init(): void {
this.delegate = new StandaloneCommandService(StandaloneServices.get(IInstantiationService));
if (this.delegate) {
this.toDispose.push(this.delegate.onWillExecuteCommand(event =>
this.onWillExecuteCommandEmitter.fire(event)
));
this.toDispose.push(this.delegate.onDidExecuteCommand(event =>
this.onDidExecuteCommandEmitter.fire(event)
));
}
}
dispose(): void {
this.toDispose.dispose();
}
get onWillExecuteCommand(): monaco.IEvent<ICommandEvent> {
return this.onWillExecuteCommandEmitter.event;
}
get onDidExecuteCommand(): monaco.IEvent<ICommandEvent> {
return this.onDidExecuteCommandEmitter.event;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async executeCommand(commandId: any, ...args: any[]): Promise<any> {
try {
await this.commandRegistry.executeCommand(commandId, ...args);
} catch (e) {
if (e.code === 'NO_ACTIVE_HANDLER') {
return this.executeMonacoCommand(commandId, ...args);
}
throw e;
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async executeMonacoCommand(commandId: any, ...args: any[]): Promise<any> {
if (this.delegate) {
return this.delegate.executeCommand(commandId, ...args);
}
throw new Error(`command '${commandId}' not found`);
}
}

View File

@@ -0,0 +1,324 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject, optional } from '@theia/core/shared/inversify';
import { Position, Location } from '@theia/core/shared/vscode-languageserver-protocol';
import { URI as CodeURI } from '@theia/core/shared/vscode-uri';
import { cloneAndChange, URI } from '@theia/core';
import { CommandContribution, CommandRegistry, CommandHandler } from '@theia/core/lib/common/command';
import { CommonCommands, QuickInputService, ApplicationShell } from '@theia/core/lib/browser';
import { EditorCommands, EditorManager, EditorWidget } from '@theia/editor/lib/browser';
import { MonacoEditor } from './monaco-editor';
import { MonacoCommandRegistry, MonacoEditorCommandHandler } from './monaco-command-registry';
import { ProtocolToMonacoConverter } from './protocol-to-monaco-converter';
import { nls } from '@theia/core/lib/common/nls';
import { EditorExtensionsRegistry } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorExtensions';
import { CommandsRegistry, ICommandService } from '@theia/monaco-editor-core/esm/vs/platform/commands/common/commands';
import * as monaco from '@theia/monaco-editor-core';
import { EndOfLineSequence } from '@theia/monaco-editor-core/esm/vs/editor/common/model';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
import { IInstantiationService } from '@theia/monaco-editor-core/esm/vs/platform/instantiation/common/instantiation';
import { ICodeEditorService } from '@theia/monaco-editor-core/esm/vs/editor/browser/services/codeEditorService';
export namespace MonacoCommands {
export const COMMON_ACTIONS = new Map<string, string>([
['editor.action.selectAll', CommonCommands.SELECT_ALL.id],
['actions.find', CommonCommands.FIND.id],
['editor.action.startFindReplaceAction', CommonCommands.REPLACE.id],
['editor.action.clipboardCutAction', CommonCommands.CUT.id],
['editor.action.clipboardCopyAction', CommonCommands.COPY.id],
['editor.action.clipboardPasteAction', CommonCommands.PASTE.id]
]);
export const GO_TO_DEFINITION = 'editor.action.revealDefinition';
export const EXCLUDE_ACTIONS = new Set([
'editor.action.quickCommand',
'editor.action.toggleStickyScroll', // Handled by `editor` package.
'undo',
'redo',
'_setContext'
]);
}
@injectable()
export class MonacoEditorCommandHandlers implements CommandContribution {
@inject(MonacoCommandRegistry)
protected readonly monacoCommandRegistry: MonacoCommandRegistry;
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
@inject(ProtocolToMonacoConverter)
protected readonly p2m: ProtocolToMonacoConverter;
@inject(QuickInputService) @optional()
protected readonly quickInputService: QuickInputService;
@inject(ApplicationShell)
protected readonly shell: ApplicationShell;
@inject(EditorManager)
protected editorManager: EditorManager;
registerCommands(): void {
this.registerMonacoCommands();
this.registerEditorCommandHandlers();
}
/**
* Register commands from Monaco to Theia registry.
*
* Monaco has different kind of commands which should be handled differently by Theia.
*
* ### Editor Actions
*
* They should be registered with a label to be visible in the quick command palette.
*
* Such actions should be enabled only if the current editor is available and
* it supports such action in the current context.
*
* ### Editor Commands
*
* Such actions should be enabled only if the current editor is available.
*
* `actions.find` and `editor.action.startFindReplaceAction` are registered as handlers for `find` and `replace`.
* If handlers are not enabled then the core should prevent the default browser behavior.
* Other Theia extensions can register alternative implementations using custom enablement.
*
* ### Global Commands
*
* These commands are not necessary dependent on the current editor and enabled always.
* But they depend on services which are global in VS Code, but bound to the editor in Monaco,
* i.e. `ICodeEditorService` or `IContextKeyService`. We should take care of providing Theia implementations for such services.
*
* #### Global Native or Editor Commands
*
* Namely: `undo`, `redo` and `editor.action.selectAll`. They depend on `ICodeEditorService`.
* They will try to delegate to the current editor and if it is not available delegate to the browser.
* They are registered as handlers for corresponding core commands always.
* Other Theia extensions can provide alternative implementations by introducing a dependency to `@theia/monaco` extension.
*
* #### Global Language Commands
*
* Like `_executeCodeActionProvider`, they depend on `ICodeEditorService` and `ITextModelService`.
*
* #### Global Context Commands
*
* It is `setContext`. It depends on `IContextKeyService`.
*
* #### Global Editor Commands
*
* Like `openReferenceToSide` and `openReference`, they depend on `IListService`.
* We treat all commands which don't match any other category of global commands as global editor commands
* and execute them using the instantiation service of the current editor.
*/
protected registerMonacoCommands(): void {
const editorActions = new Map([...EditorExtensionsRegistry.getEditorActions()].map(({ id, label, alias }) => [id, { label, alias }]));
const codeEditorService = StandaloneServices.get(ICodeEditorService);
const globalInstantiationService = StandaloneServices.get(IInstantiationService);
const monacoCommands = CommandsRegistry.getCommands();
for (const id of monacoCommands.keys()) {
if (MonacoCommands.EXCLUDE_ACTIONS.has(id)) {
continue;
}
const handler: CommandHandler = {
execute: (...args) => {
/*
* We check monaco focused code editor first since they can contain inline like the debug console and embedded editors like in the peek reference.
* If there is not such then we check last focused editor tracked by us.
*/
const editor = codeEditorService.getFocusedCodeEditor() || codeEditorService.getActiveCodeEditor();
if (editorActions.has(id)) {
const action = editor && editor.getAction(id);
if (!action) {
return;
}
return action.run();
}
if (!globalInstantiationService) {
return;
}
return globalInstantiationService.invokeFunction(
monacoCommands.get(id)!.handler,
...args
);
},
isEnabled: () => {
const editor = codeEditorService.getFocusedCodeEditor() || codeEditorService.getActiveCodeEditor();
if (editorActions.has(id)) {
const action = editor && editor.getAction(id);
return !!action && action.isSupported();
}
if (!!EditorExtensionsRegistry.getEditorCommand(id) || MonacoCommands.COMMON_ACTIONS.has(id)) {
return !!editor;
}
return true;
}
};
const commandAction = editorActions.get(id);
this.commandRegistry.registerCommand({ id, label: commandAction?.label, originalLabel: commandAction?.alias }, handler);
const coreCommand = MonacoCommands.COMMON_ACTIONS.get(id);
if (coreCommand) {
this.commandRegistry.registerHandler(coreCommand, handler);
}
}
// the _setContext command stringifies all URIs in contextValue and needs to be overriden to handle all URI types in Theia
const setContext = monacoCommands.get('_setContext');
if (setContext) {
this.commandRegistry.registerCommand({ id: setContext.id }, {
execute: (contextKey, contextValue, ...args) => globalInstantiationService.invokeFunction(setContext.handler,
contextKey,
cloneAndChange(contextValue, orig => {
if (orig instanceof URI || CodeURI.isUri(orig)) {
return orig.toString();
}
return undefined;
}),
...args
)
});
}
}
protected registerEditorCommandHandlers(): void {
this.monacoCommandRegistry.registerHandler(EditorCommands.SHOW_REFERENCES.id, this.newShowReferenceHandler());
this.monacoCommandRegistry.registerHandler(EditorCommands.CONFIG_INDENTATION.id, this.newConfigIndentationHandler());
this.monacoCommandRegistry.registerHandler(EditorCommands.CONFIG_EOL.id, this.newConfigEolHandler());
this.monacoCommandRegistry.registerHandler(EditorCommands.INDENT_USING_SPACES.id, this.newConfigTabSizeHandler(true));
this.monacoCommandRegistry.registerHandler(EditorCommands.INDENT_USING_TABS.id, this.newConfigTabSizeHandler(false));
this.monacoCommandRegistry.registerHandler(EditorCommands.REVERT_EDITOR.id, this.newRevertActiveEditorHandler());
this.monacoCommandRegistry.registerHandler(EditorCommands.REVERT_AND_CLOSE.id, this.newRevertAndCloseActiveEditorHandler());
}
protected newShowReferenceHandler(): MonacoEditorCommandHandler {
return {
execute: (editor: MonacoEditor, uri: string, position: Position, locations: Location[]) => {
StandaloneServices.get(ICommandService).executeCommand(
'editor.action.showReferences',
monaco.Uri.parse(uri),
this.p2m.asPosition(position),
locations.map(l => this.p2m.asLocation(l))
);
}
};
}
protected newConfigIndentationHandler(): MonacoEditorCommandHandler {
return {
execute: editor => this.configureIndentation(editor)
};
}
protected configureIndentation(editor: MonacoEditor): void {
const items = [true, false].map(useSpaces => ({
label: nls.localizeByDefault(`Indent Using ${useSpaces ? 'Spaces' : 'Tabs'}`),
execute: () => this.configureTabSize(editor, useSpaces)
}));
this.quickInputService?.showQuickPick(items, { placeholder: nls.localizeByDefault('Select Action') });
}
protected newConfigEolHandler(): MonacoEditorCommandHandler {
return {
execute: editor => this.configureEol(editor)
};
}
protected configureEol(editor: MonacoEditor): void {
const items = ['LF', 'CRLF'].map(lineEnding =>
({
label: lineEnding,
execute: () => this.setEol(editor, lineEnding)
})
);
this.quickInputService?.showQuickPick(items, { placeholder: nls.localizeByDefault('Select End of Line Sequence') });
}
protected setEol(editor: MonacoEditor, lineEnding: string): void {
const model = editor.document && editor.document.textEditorModel;
if (model) {
if (lineEnding === 'CRLF' || lineEnding === '\r\n') {
model.pushEOL(EndOfLineSequence.CRLF);
} else {
model.pushEOL(EndOfLineSequence.LF);
}
}
}
protected newConfigTabSizeHandler(useSpaces: boolean): MonacoEditorCommandHandler {
return {
execute: editor => this.configureTabSize(editor, useSpaces)
};
}
protected configureTabSize(editor: MonacoEditor, useSpaces: boolean): void {
const model = editor.document && editor.document.textEditorModel;
if (model) {
const { tabSize } = model.getOptions();
const sizes = Array.from(Array(8), (_, x) => x + 1);
const tabSizeOptions = sizes.map(size =>
({
label: size === tabSize ? size + ' ' + nls.localizeByDefault('Configured Tab Size') : size.toString(),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
execute: () =>
model.updateOptions({
tabSize: size || tabSize,
indentSize: size || tabSize,
insertSpaces: useSpaces
})
})
);
this.quickInputService?.showQuickPick(tabSizeOptions, { placeholder: nls.localizeByDefault('Select Tab Size for Current File') });
}
}
protected newRevertActiveEditorHandler(): MonacoEditorCommandHandler {
return {
execute: () => this.revertEditor(this.getActiveEditor().editor),
};
}
protected newRevertAndCloseActiveEditorHandler(): MonacoEditorCommandHandler {
return {
execute: async () => this.revertAndCloseActiveEditor(this.getActiveEditor())
};
}
protected getActiveEditor(): { widget?: EditorWidget, editor?: MonacoEditor } {
const widget = this.editorManager.currentEditor;
return { widget, editor: widget && MonacoEditor.getCurrent(this.editorManager) };
}
protected async revertEditor(editor?: MonacoEditor): Promise<void> {
if (editor) {
return editor.document.revert();
}
}
protected async revertAndCloseActiveEditor(current: { widget?: EditorWidget, editor?: MonacoEditor }): Promise<void> {
if (current.editor && current.widget) {
try {
await this.revertEditor(current.editor);
current.widget.close();
} catch (error) {
await this.shell.closeWidget(current.widget.id, { save: false });
}
}
}
}

View File

@@ -0,0 +1,169 @@
// *****************************************************************************
// Copyright (C) 2019 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, postConstruct } from '@theia/core/shared/inversify';
import {
ContextKeyService as TheiaContextKeyService, ContextKey, ContextKeyChangeEvent,
ScopedValueStore, ContextMatcher, ContextKeyValue, Context
} from '@theia/core/lib/browser/context-key-service';
import { Emitter, Event } from '@theia/core';
import { AbstractContextKeyService, Context as MonacoContext } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/browser/contextKeyService';
import { ContextKeyExpr, ContextKeyExpression, IContext, IContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/common/contextkey';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
@injectable()
export class MonacoContextKeyService implements TheiaContextKeyService {
protected readonly onDidChangeEmitter = new Emitter<ContextKeyChangeEvent>();
get onDidChange(): Event<ContextKeyChangeEvent> {
if (this.activeContext && 'onDidChange' in this.activeContext && this.activeContext.onDidChange) {
return this.activeContext.onDidChange;
}
return this.onDidChangeEmitter.event;
}
get contextKeyService(): AbstractContextKeyService {
return StandaloneServices.get(IContextKeyService) as AbstractContextKeyService;
}
@postConstruct()
protected init(): void {
this.contextKeyService.onDidChangeContext(e =>
this.onDidChangeEmitter.fire({
affects: keys => e.affectsSome(keys)
})
);
}
createKey<T extends ContextKeyValue>(key: string, defaultValue: T | undefined): ContextKey<T> {
return this.contextKeyService.createKey(key, defaultValue);
}
activeContext?: HTMLElement | IContext | Context;
match(expression: string, context?: HTMLElement): boolean {
const parsed = this.parse(expression);
if (parsed) {
const ctx = this.identifyContext(context);
if (!ctx) {
return this.contextKeyService.contextMatchesRules(parsed);
}
return parsed.evaluate(ctx);
}
return true;
}
protected identifyContext(callersContext?: HTMLElement | IContext, service: IContextKeyService = this.contextKeyService): IContext | undefined {
if (callersContext && 'getValue' in callersContext) {
return callersContext;
} else if (this.activeContext && 'getValue' in this.activeContext) {
return this.activeContext;
}
const browserContext = callersContext ?? this.activeContext ?? (document.activeElement instanceof HTMLElement ? document.activeElement : undefined);
if (browserContext) {
return service.getContext(browserContext);
}
return undefined;
}
protected readonly expressions = new Map<string, ContextKeyExpression>();
parse(when: string): ContextKeyExpression | undefined {
let expression = this.expressions.get(when);
if (!expression) {
expression = ContextKeyExpr.deserialize(when);
if (expression) {
this.expressions.set(when, expression);
}
}
return expression;
}
parseKeys(expression: string): Set<string> | undefined {
const expr = ContextKeyExpr.deserialize(expression);
return expr ? new Set<string>(expr.keys()) : expr;
}
with<T>(values: Record<string, unknown>, callback: () => T): T {
const id = this.contextKeyService.createChildContext();
const child = this.contextKeyService.getContextValuesContainer(id);
for (const [key, value] of Object.entries(values)) {
child.setValue(key, value);
}
try {
return this.withContext(child, callback);
} finally {
this.contextKeyService.disposeContext(id);
}
}
withContext<T>(context: Context, callback: () => T): T {
const oldActive = this.activeContext;
this.activeContext = context;
try {
return callback();
} finally {
this.activeContext = oldActive;
}
}
createScoped(target: HTMLElement): ScopedValueStore {
const scoped = this.contextKeyService.createScoped(target);
if (scoped instanceof AbstractContextKeyService) {
return scoped as unknown as ScopedValueStore;
}
throw new Error('Could not created scoped value store');
}
createOverlay(overlay: Iterable<[string, unknown]>): ContextMatcher {
const delegate = this.contextKeyService.createOverlay(overlay);
return {
match: (expression: string, context?: HTMLElement) => {
const parsed = this.parse(expression);
if (parsed) {
const ctx = this.identifyContext(context, delegate);
if (!ctx) {
return delegate.contextMatchesRules(parsed);
}
return parsed.evaluate(ctx);
}
return true;
}
};
}
setContext(key: string, value: unknown): void {
this.contextKeyService.setContext(key, value);
}
getLocalContextKeys(element: HTMLElement): Set<string> {
const context = this.contextKeyService.getContext(element);
// Get local values
if (context && context instanceof MonacoContext) {
const localValues = context.value;
// Filter out the internal '_contextId' key
const res = new Set(Object.keys(localValues));
res.delete('_contextId');
return res;
}
// For other IContext implementations (like NullContext), return empty set
return new Set<string>();
}
dispose(): void {
this.activeContext = undefined;
this.onDidChangeEmitter.dispose();
this.contextKeyService.dispose();
}
}

View File

@@ -0,0 +1,132 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject } from '@theia/core/shared/inversify';
import { MenuPath } from '@theia/core/lib/common/menu';
import { EDITOR_CONTEXT_MENU } from '@theia/editor/lib/browser';
import { Anchor, ContextMenuAccess, ContextMenuRenderer, Coordinate } from '@theia/core/lib/browser';
import { Menu } from '@theia/core/shared/@lumino/widgets';
import { CommandRegistry } from '@theia/core/shared/@lumino/commands';
import { IContextMenuService } from '@theia/monaco-editor-core/esm/vs/platform/contextview/browser/contextView';
import { IContextMenuDelegate } from '@theia/monaco-editor-core/esm/vs/base/browser/contextmenu';
import { IAction, Separator, SubmenuAction } from '@theia/monaco-editor-core/esm/vs/base/common/actions';
import { MenuItemAction } from '@theia/monaco-editor-core/esm/vs/platform/actions/common/actions';
import { Event, Emitter } from '@theia/monaco-editor-core/esm/vs/base/common/event';
import { StandardMouseEvent } from '@theia/monaco-editor-core/esm/vs/base/browser/mouseEvent';
@injectable()
export class MonacoContextMenuService implements IContextMenuService {
declare readonly _serviceBrand: undefined;
protected readonly onDidShowContextMenuEmitter = new Emitter<void>();
get onDidShowContextMenu(): Event<void> {
return this.onDidShowContextMenuEmitter.event;
};
protected readonly onDidHideContextMenuEmitter = new Emitter<void>();
get onDidHideContextMenu(): Event<void> {
return this.onDidShowContextMenuEmitter.event;
};
@inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer;
toAnchor(anchor: HTMLElement | Coordinate | StandardMouseEvent): Anchor {
if (anchor instanceof HTMLElement) {
return { x: anchor.offsetLeft, y: anchor.offsetTop };
} else if (anchor instanceof StandardMouseEvent) {
return { x: anchor.posx, y: anchor.posy };
} else {
return anchor;
}
}
private getContext(delegate: IContextMenuDelegate): HTMLElement {
const anchor = delegate.getAnchor();
if (anchor instanceof HTMLElement) {
return anchor;
} else if (anchor instanceof StandardMouseEvent) {
return anchor.target;
} else {
return window.document.body; // last resort
}
}
showContextMenu(delegate: IContextMenuDelegate): void {
const anchor = this.toAnchor(delegate.getAnchor());
const actions = delegate.getActions();
const context = this.getContext(delegate);
const onHide = () => {
delegate.onHide?.(false);
this.onDidHideContextMenuEmitter.fire();
};
// Actions for editor context menu come as 'MenuItemAction' items
// In case of 'Quick Fix' actions come as 'CodeActionAction' items
if (actions.length > 0 && actions[0] instanceof MenuItemAction) {
this.contextMenuRenderer.render({
context: context,
menuPath: this.menuPath(),
anchor,
onHide
});
} else {
const menu = new Menu({ commands: new CommandRegistry() });
this.populateMenu(menu, actions);
menu.aboutToClose.connect(() => onHide());
menu.open(anchor.x, anchor.y);
this.contextMenuRenderer.current = new ContextMenuAccess(menu);
}
this.onDidShowContextMenuEmitter.fire();
}
protected populateMenu(menu: Menu, actions: readonly IAction[]): void {
for (const action of actions) {
if (action instanceof SubmenuAction) {
const submenu = new Menu({ commands: new CommandRegistry() });
submenu.title.label = action.label;
submenu.title.caption = action.tooltip;
if (action.class) {
submenu.addClass(action.class);
}
this.populateMenu(submenu, action.actions);
menu.addItem({
type: 'submenu',
submenu
});
} else if (action instanceof Separator) {
menu.addItem({
type: 'separator'
});
} else {
menu.commands.addCommand(action.id, {
label: action.label,
className: action.class,
isToggled: () => Boolean(action.checked),
isEnabled: () => action.enabled,
execute: () => action.run()
});
menu.addItem({
type: 'command',
command: action.id
});
}
}
}
protected menuPath(): MenuPath {
return EDITOR_CONTEXT_MENU;
}
}

View File

@@ -0,0 +1,62 @@
// *****************************************************************************
// Copyright (C) 2025 1C-Soft LLC and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { Diff, DiffComputer } from '@theia/core/lib/common/diff';
import URI from '@theia/core/lib/common/uri';
import { Range } from '@theia/core/shared/vscode-languageserver-protocol';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
import { IEditorWorkerService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/editorWorker';
@injectable()
export class MonacoDiffComputer implements DiffComputer {
async computeDiff(left: URI, right: URI): Promise<Diff | undefined> {
const diff = await StandaloneServices.get(IEditorWorkerService).computeDiff(
left['codeUri'],
right['codeUri'],
{
ignoreTrimWhitespace: false,
maxComputationTimeMs: 0,
computeMoves: false,
},
'advanced'
);
if (!diff) {
return undefined;
}
const convertLineRange = (r: { startLineNumber: number; endLineNumberExclusive: number }) => ({
start: r.startLineNumber - 1,
end: r.endLineNumberExclusive - 1
});
const convertRange = (r: { startLineNumber: number; startColumn: number; endLineNumber: number; endColumn: number }) =>
Range.create(r.startLineNumber - 1, r.startColumn - 1, r.endLineNumber - 1, r.endColumn - 1);
const changes = diff.changes.map(c => ({
left: convertLineRange(c.original),
right: convertLineRange(c.modified),
innerChanges: c.innerChanges?.map(ic => ({
left: convertRange(ic.originalRange),
right: convertRange(ic.modifiedRange)
}))
}));
return { changes };
}
}

View File

@@ -0,0 +1,175 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import URI from '@theia/core/lib/common/uri';
import { Disposable } from '@theia/core/lib/common';
import { Dimension, DiffNavigator, DeltaDecorationParams } from '@theia/editor/lib/browser';
import { MonacoEditorModel } from './monaco-editor-model';
import { EditorServiceOverrides, MonacoEditor, MonacoEditorServices } from './monaco-editor';
import { MonacoDiffNavigatorFactory } from './monaco-diff-navigator-factory';
import { DiffUris } from '@theia/core/lib/browser/diff-uris';
import * as monaco from '@theia/monaco-editor-core';
import { ICodeEditor, IDiffEditorConstructionOptions } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorBrowser';
import { IActionDescriptor, IStandaloneCodeEditor, IStandaloneDiffEditor, StandaloneCodeEditor, StandaloneDiffEditor2 }
from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneCodeEditor';
import { IEditorConstructionOptions } from '@theia/monaco-editor-core/esm/vs/editor/browser/config/editorConfiguration';
import { EmbeddedDiffEditorWidget } from '@theia/monaco-editor-core/esm/vs/editor/browser/widget/diffEditor/embeddedDiffEditorWidget';
import { IInstantiationService } from '@theia/monaco-editor-core/esm/vs/platform/instantiation/common/instantiation';
import { ContextKeyValue, IContextKey } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/common/contextkey';
import { IDisposable } from '@theia/monaco-editor-core/esm/vs/base/common/lifecycle';
import { ICommandHandler } from '@theia/monaco-editor-core/esm/vs/platform/commands/common/commands';
import { EditorContextKeys } from '@theia/monaco-editor-core/esm/vs/editor/common/editorContextKeys';
import { IEditorOptions } from '@theia/monaco-editor-core/esm/vs/editor/common/config/editorOptions';
import { ILineChange } from '@theia/monaco-editor-core/esm/vs/editor/common/diff/legacyLinesDiffComputer';
export namespace MonacoDiffEditor {
export interface IOptions extends MonacoEditor.ICommonOptions, IDiffEditorConstructionOptions {
}
}
export class MonacoDiffEditor extends MonacoEditor {
protected _diffEditor: IStandaloneDiffEditor;
protected _diffNavigator: DiffNavigator;
protected readonly diffEditorModel: monaco.editor.IDiffEditorModel;
constructor(
uri: URI,
node: HTMLElement,
readonly originalModel: MonacoEditorModel,
readonly modifiedModel: MonacoEditorModel,
services: MonacoEditorServices,
protected readonly diffNavigatorFactory: MonacoDiffNavigatorFactory,
options?: MonacoDiffEditor.IOptions,
override?: EditorServiceOverrides,
parentEditor?: MonacoEditor
) {
super(uri, modifiedModel, node, services, options, override, parentEditor);
this.diffEditorModel = { original: this.originalModel.textEditorModel, modified: this.modifiedModel.textEditorModel };
this.documents.add(originalModel);
this.wordWrapOverride = options?.wordWrapOverride2;
this._diffNavigator = diffNavigatorFactory.createdDiffNavigator(this._diffEditor);
if (parentEditor) {
// Embedded diff editors don't participate in visibility tracking (they're not wrapped in EditorWidget),
// so we need to set the model immediately since handleVisibilityChanged will never be called.
this.diffEditor.setModel(this.diffEditorModel);
}
}
get diffEditor(): monaco.editor.IStandaloneDiffEditor {
return this._diffEditor as unknown as monaco.editor.IStandaloneDiffEditor;
}
get diffNavigator(): DiffNavigator {
return this._diffNavigator;
}
get diffInformation(): ILineChange[] {
return this._diffEditor.getLineChanges() || [];
}
protected override create(options?: IDiffEditorConstructionOptions, override?: EditorServiceOverrides): Disposable {
options = { ...options, fixedOverflowWidgets: true };
const instantiator = this.getInstantiatorWithOverrides(override);
/**
* @monaco-uplift. Should be guaranteed to work.
* Incomparable enums prevent TypeScript from believing that public IStandaloneDiffEditor is satisfied by private StandaloneDiffEditor
*/
this._diffEditor = this.parentEditor ?
instantiator.createInstance(EmbeddedDiffEditor, this.node, options, {}, this.parentEditor.getControl() as unknown as ICodeEditor) :
instantiator.createInstance(StandaloneDiffEditor2, this.node, options);
this.editor = this._diffEditor.getModifiedEditor() as unknown as monaco.editor.IStandaloneCodeEditor;
return this._diffEditor;
}
protected wordWrapOverride: IEditorOptions['wordWrapOverride2'];
protected lastReachedSideBySideBreakpoint = true;
protected override resize(dimension: Dimension | null): void {
if (this.node) {
const layoutSize = this.computeLayoutSize(this.node, dimension);
this._diffEditor.layout(layoutSize);
// Workaround for https://github.com/microsoft/vscode/issues/217386#issuecomment-2711750462
const leftEditor = this._diffEditor.getOriginalEditor();
const hasReachedSideBySideBreakpoint = leftEditor.contextKeyService
.getContextKeyValue(EditorContextKeys.diffEditorRenderSideBySideInlineBreakpointReached.key);
if (hasReachedSideBySideBreakpoint !== this.lastReachedSideBySideBreakpoint) {
leftEditor.updateOptions({ wordWrapOverride2: this.wordWrapOverride ?? hasReachedSideBySideBreakpoint ? 'off' : 'inherit' });
}
this.lastReachedSideBySideBreakpoint = !!hasReachedSideBySideBreakpoint;
}
}
override isActionSupported(id: string): boolean {
const action = this._diffEditor.getSupportedActions().find(a => a.id === id);
return !!action && action.isSupported() && super.isActionSupported(id);
}
override deltaDecorations(params: DeltaDecorationParams): string[] {
console.warn('`deltaDecorations` should be called on either the original, or the modified editor.');
return [];
}
override getResourceUri(): URI {
return new URI(this.originalModel.uri);
}
override createMoveToUri(resourceUri: URI): URI {
const [left, right] = DiffUris.decode(this.uri);
return DiffUris.encode(left.withPath(resourceUri.path), right.withPath(resourceUri.path));
}
override readonly onShouldDisplayDirtyDiffChanged = undefined;
override shouldDisplayDirtyDiff(): boolean {
return false;
}
override setShouldDisplayDirtyDiff(value: boolean): void {
// no op
}
protected override get baseEditor(): monaco.editor.IEditor {
return this.diffEditor;
}
protected override get baseModel(): monaco.editor.IEditorModel {
return this.diffEditorModel;
}
}
class EmbeddedDiffEditor extends EmbeddedDiffEditorWidget implements IStandaloneDiffEditor {
protected override _createInnerEditor(instantiationService: IInstantiationService, container: HTMLElement,
options: Readonly<IEditorConstructionOptions>): StandaloneCodeEditor {
return instantiationService.createInstance(StandaloneCodeEditor, container, options);
}
override getOriginalEditor(): IStandaloneCodeEditor {
return super.getOriginalEditor() as IStandaloneCodeEditor;
}
override getModifiedEditor(): IStandaloneCodeEditor {
return super.getModifiedEditor() as IStandaloneCodeEditor;
}
addCommand(keybinding: number, handler: ICommandHandler, context?: string): string | null {
return this.getModifiedEditor().addCommand(keybinding, handler, context);
}
createContextKey<T extends ContextKeyValue = ContextKeyValue>(key: string, defaultValue: T): IContextKey<T> {
return this.getModifiedEditor().createContextKey(key, defaultValue);
}
addAction(descriptor: IActionDescriptor): IDisposable {
return this.getModifiedEditor().addAction(descriptor);
}
}

View File

@@ -0,0 +1,39 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { DiffNavigator } from '@theia/editor/lib/browser';
import { IDiffEditor } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorBrowser';
@injectable()
export class MonacoDiffNavigatorFactory {
static nullNavigator = <DiffNavigator>{
hasNext: () => false,
hasPrevious: () => false,
next: () => { },
previous: () => { },
};
createdDiffNavigator(editor: IDiffEditor): DiffNavigator {
return {
hasNext: () => true,
hasPrevious: () => true,
next: () => editor.goToDiff('next'),
previous: () => editor.goToDiff('previous')
};
}
}

View File

@@ -0,0 +1,127 @@
// *****************************************************************************
// Copyright (C) 2025 1C-Soft LLC and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { inject, injectable } from '@theia/core/shared/inversify';
import { ArrayUtils, CommandMenu, CommandRegistry, CompoundMenuNode, Disposable, Event, MenuModelRegistry, MenuNode } from '@theia/core';
import { ObservableFromEvent, ObservableUtils } from '@theia/core/lib/common/observable';
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { Context } from '@theia/core/lib/browser/context-key-service';
import { EditorManager, EDITOR_CONTENT_MENU, EditorWidget } from '@theia/editor/lib/browser';
import { ICodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorBrowser';
import { IContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/common/contextkey';
import { MonacoContextKeyService } from './monaco-context-key-service';
import { MonacoEditor } from './monaco-editor';
import { MonacoEditorOverlayButton } from './monaco-editor-overlay-button';
/**
* Implements {@link EDITOR_CONTENT_MENU} for {@link MonacoEditor}s.
*/
@injectable()
export class MonacoEditorContentMenuContribution implements FrontendApplicationContribution {
@inject(EditorManager)
protected readonly editorManager: EditorManager;
@inject(MenuModelRegistry)
protected readonly menus: MenuModelRegistry;
@inject(CommandRegistry)
protected readonly commands: CommandRegistry;
@inject(MonacoContextKeyService)
protected readonly contextKeyService: MonacoContextKeyService;
onStart(): void {
this.editorManager.onCreated(editorWidget => {
const editor = MonacoEditor.get(editorWidget);
if (editor) {
const disposable = this.createEditorContentMenu(editor, editorWidget);
editor.onDispose(() => disposable.dispose());
}
});
}
protected createEditorContentMenu(editor: MonacoEditor, editorWidget: EditorWidget): Disposable {
const contextKeyService = (editor.getControl() as unknown as ICodeEditor).invokeWithinContext( // get the editor-scoped context key service
accessor => accessor.get(IContextKeyService)
);
const context: Context = {
getValue: key => contextKeyService.getContextKeyValue(key),
onDidChange: Event.map(contextKeyService.onDidChangeContext, event => ({
affects: keys => event.affectsSome(keys)
}))
};
const menuNodesObservable = ObservableFromEvent.create(this.menus.onDidChange,
() => this.getEditorContentMenuNodes(),
{ isEqual: (a, b) => ArrayUtils.equals(a, b) }
);
return ObservableUtils.autorunWithDisposables(({ toDispose }) => {
const menuNodes = menuNodesObservable.get();
const firstMatchObservable = ObservableFromEvent.create(contextKeyService.onDidChangeContext, () => this.withContext(context,
() => menuNodes.find(menuNode => menuNode.isVisible(EDITOR_CONTENT_MENU, this.contextKeyService, undefined, editorWidget))
));
// eslint-disable-next-line @typescript-eslint/no-shadow
toDispose.push(ObservableUtils.autorunWithDisposables(({ toDispose }) => {
const firstMatch = firstMatchObservable.get();
if (firstMatch) {
const button = new MonacoEditorOverlayButton(editor, firstMatch.label);
toDispose.push(button);
toDispose.push(button.onClick(() =>
this.withContext(context, () => firstMatch.run(EDITOR_CONTENT_MENU, editorWidget))
));
const handlersObservable = ObservableFromEvent.create(this.commands.onCommandsChanged,
() => this.commands.getAllHandlers(firstMatch.id),
{ isEqual: (a, b) => ArrayUtils.equals(a, b) }
);
// eslint-disable-next-line @typescript-eslint/no-shadow
toDispose.push(ObservableUtils.autorunWithDisposables(({ toDispose }) => {
this.withContext(context, () => {
button.enabled = firstMatch.isEnabled(EDITOR_CONTENT_MENU, editorWidget);
const handlers = handlersObservable.get();
for (const handler of handlers) {
const { onDidChangeEnabled } = handler;
if (onDidChangeEnabled) {
// for handlers with declarative enablement such as those originating from `PluginContributionHandler.registerCommand`,
// the onDidChangeEnabled event is context-dependent, so we need to ensure the subscription is made within `withContext`
toDispose.push(onDidChangeEnabled(() => this.withContext(context, () =>
button.enabled = firstMatch.isEnabled(EDITOR_CONTENT_MENU, editorWidget)
)));
}
}
});
}));
}
}));
});
}
protected getEditorContentMenuNodes(): CommandMenu[] {
const result: CommandMenu[] = [];
const children = this.menus.getMenu(EDITOR_CONTENT_MENU)?.children ?? [];
const getCommandMenuNodes = (nodes: MenuNode[]) => nodes.filter(CommandMenu.is);
// inline the special navigation group, if any; the navigation group would always be the first element
if (children.length && CompoundMenuNode.isNavigationGroup(children[0])) {
result.push(...getCommandMenuNodes(children[0].children));
}
result.push(...getCommandMenuNodes(children));
return result;
}
protected withContext<T>(context: Context, callback: () => T): T {
return this.contextKeyService.withContext(context, callback);
}
}

View File

@@ -0,0 +1,655 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { Position, Range, TextDocumentSaveReason } from '@theia/core/shared/vscode-languageserver-protocol';
import { TextEditorDocument, EncodingMode, FindMatchesOptions, FindMatch } from '@theia/editor/lib/browser';
import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposable';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { CancellationTokenSource, CancellationToken } from '@theia/core/lib/common/cancellation';
import { Resource, ResourceError, ResourceVersion } from '@theia/core/lib/common/resource';
import { Saveable, SaveOptions, SaveReason } from '@theia/core/lib/browser/saveable';
import { MonacoToProtocolConverter } from './monaco-to-protocol-converter';
import { ProtocolToMonacoConverter } from './protocol-to-monaco-converter';
import { ILogger, Loggable, Log } from '@theia/core/lib/common/logger';
import { ITextBufferFactory, ITextModel, ITextSnapshot } from '@theia/monaco-editor-core/esm/vs/editor/common/model';
import { IResolvedTextEditorModel } from '@theia/monaco-editor-core/esm/vs/editor/common/services/resolverService';
import * as monaco from '@theia/monaco-editor-core';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
import { ILanguageService } from '@theia/monaco-editor-core/esm/vs/editor/common/languages/language';
import { IModelService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/model';
import { createTextBufferFactoryFromStream } from '@theia/monaco-editor-core/esm/vs/editor/common/model/textModel';
import { editorGeneratedPreferenceProperties } from '@theia/editor/lib/common/editor-generated-preference-schema';
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
import { BinaryBuffer } from '@theia/core/lib/common/buffer';
import { Listener, ListenerList } from '@theia/core';
import { EditorPreferences } from '@theia/editor/lib/common/editor-preferences';
export {
TextDocumentSaveReason
};
export interface WillSaveMonacoModelEvent {
model: MonacoEditorModel,
token: CancellationToken,
options?: SaveOptions
}
export interface MonacoModelContentChangedEvent {
readonly model: MonacoEditorModel;
readonly contentChanges: MonacoTextDocumentContentChange[];
}
export interface MonacoTextDocumentContentChange {
readonly range: Range;
readonly rangeOffset: number;
readonly rangeLength: number;
readonly text: string;
}
export class MonacoEditorModel implements IResolvedTextEditorModel, TextEditorDocument {
suppressOpenEditorWhenDirty = false;
lineNumbersMinChars = 3;
/* @deprecated there is no general save timeout, each participant should introduce a sensible timeout */
readonly onWillSaveLoopTimeOut = 1500;
protected bufferSavedVersionId: number;
protected model: ITextModel;
protected readonly resolveModel: Promise<void>;
protected readonly toDispose = new DisposableCollection();
protected readonly toDisposeOnAutoSave = new DisposableCollection();
protected readonly onDidChangeContentEmitter = new Emitter<MonacoModelContentChangedEvent>();
readonly onDidChangeContent = this.onDidChangeContentEmitter.event;
get onContentChanged(): Event<void> {
return (listener, thisArgs, disposables) => this.onDidChangeContent(() => listener(), thisArgs, disposables);
}
protected readonly onDidSaveModelEmitter = new Emitter<ITextModel>();
readonly onDidSaveModel = this.onDidSaveModelEmitter.event;
protected readonly onDidChangeValidEmitter = new Emitter<void>();
readonly onDidChangeValid = this.onDidChangeValidEmitter.event;
protected readonly onDidChangeEncodingEmitter = new Emitter<string>();
readonly onDidChangeEncoding = this.onDidChangeEncodingEmitter.event;
readonly onDidChangeReadOnly: Event<boolean | MarkdownString> = this.resource.onDidChangeReadOnly ?? Event.None;
private preferredEncoding: string | undefined;
private contentEncoding: string | undefined;
protected resourceVersion: ResourceVersion | undefined;
protected readonly onWillSaveModelListeners: ListenerList<WillSaveMonacoModelEvent, Promise<void>> = new ListenerList;
readonly onModelWillSaveModel = this.onWillSaveModelListeners.registration;
constructor(
protected readonly resource: Resource,
protected readonly m2p: MonacoToProtocolConverter,
protected readonly p2m: ProtocolToMonacoConverter,
protected readonly logger?: ILogger,
protected readonly editorPreferences?: EditorPreferences
) {
this.toDispose.push(resource);
this.toDispose.push(this.toDisposeOnAutoSave);
this.toDispose.push(this.onDidChangeContentEmitter);
this.toDispose.push(this.onDidSaveModelEmitter);
this.toDispose.push(this.onDirtyChangedEmitter);
this.toDispose.push(this.onDidChangeEncodingEmitter);
this.toDispose.push(this.onDidChangeValidEmitter);
this.toDispose.push(Disposable.create(() => this.cancelSave()));
this.toDispose.push(Disposable.create(() => this.cancelSync()));
this.resolveModel = this.readContents().then(
content => this.initialize(content || '')
);
}
undo(): void {
this.model.undo();
}
redo(): void {
this.model.redo();
}
dispose(): void {
this.toDispose.dispose();
}
isDisposed(): boolean {
return this.toDispose.disposed;
}
resolve(): Promise<void> {
return this.resolveModel;
}
isResolved(): boolean {
return Boolean(this.model);
}
setEncoding(encoding: string, mode: EncodingMode): Promise<void> {
if (mode === EncodingMode.Decode && this.dirty) {
return Promise.resolve();
}
if (!this.setPreferredEncoding(encoding)) {
return Promise.resolve();
}
if (mode === EncodingMode.Decode) {
return this.sync();
}
return this.scheduleSave(this.cancelSave(), true, { saveReason: SaveReason.Manual });
}
getEncoding(): string | undefined {
return this.preferredEncoding || this.contentEncoding;
}
protected setPreferredEncoding(encoding: string): boolean {
if (encoding === this.preferredEncoding || (!this.preferredEncoding && encoding === this.contentEncoding)) {
return false;
}
this.preferredEncoding = encoding;
this.onDidChangeEncodingEmitter.fire(encoding);
return true;
}
protected updateContentEncoding(): void {
const contentEncoding = this.resource.encoding;
if (!contentEncoding || this.contentEncoding === contentEncoding) {
return;
}
this.contentEncoding = contentEncoding;
if (!this.preferredEncoding) {
this.onDidChangeEncodingEmitter.fire(contentEncoding);
}
}
/**
* #### Important
* Only this method can create an instance of `monaco.editor.IModel`,
* there should not be other calls to `monaco.editor.createModel`.
*/
protected initialize(value: string | ITextBufferFactory): void {
if (!this.toDispose.disposed) {
const uri = monaco.Uri.parse(this.resource.uri.toString());
let firstLine;
if (typeof value === 'string') {
firstLine = value;
const firstLF = value.indexOf('\n');
if (firstLF !== -1) {
firstLine = value.substring(0, firstLF);
}
} else {
firstLine = value.getFirstLineText(1000);
}
const languageSelection = StandaloneServices.get(ILanguageService).createByFilepathOrFirstLine(uri, firstLine);
this.model = StandaloneServices.get(IModelService).createModel(value, languageSelection, uri);
this.resourceVersion = this.resource.version;
this.setDirty(this._dirty || (!!this.resource.initiallyDirty));
this.updateSavedVersionId();
this.toDispose.push(this.model);
this.toDispose.push(this.model.onDidChangeContent(event => this.fireDidChangeContent(event)));
if (this.resource.onDidChangeContents) {
this.toDispose.push(this.resource.onDidChangeContents(() => this.sync()));
}
}
}
/**
* Use `valid` to access it.
* Use `setValid` to mutate it.
*/
protected _valid = false;
/**
* Whether it is possible to load content from the underlying resource.
*/
get valid(): boolean {
return this._valid;
}
protected setValid(valid: boolean): void {
if (valid === this._valid) {
return;
}
this._valid = valid;
this.onDidChangeValidEmitter.fire(undefined);
}
protected _dirty = false;
get dirty(): boolean {
return this._dirty;
}
protected setDirty(dirty: boolean): void {
if (dirty === this._dirty) {
return;
}
this._dirty = dirty;
if (dirty === false) {
this.updateSavedVersionId();
}
this.onDirtyChangedEmitter.fire(undefined);
}
private updateSavedVersionId(): void {
this.bufferSavedVersionId = this.model.getAlternativeVersionId();
}
protected readonly onDirtyChangedEmitter = new Emitter<void>();
get onDirtyChanged(): Event<void> {
return this.onDirtyChangedEmitter.event;
}
get uri(): string {
return this.resource.uri.toString();
}
get autosaveable(): boolean | undefined {
return this.resource.autosaveable;
}
protected _languageId: string | undefined;
get languageId(): string {
return this._languageId !== undefined ? this._languageId : this.model.getLanguageId();
}
getLanguageId(): string | undefined {
return this.languageId;
}
/**
* It's a hack to dispatch close notification with an old language id; don't use it.
*/
setLanguageId(languageId: string | undefined): void {
this._languageId = languageId;
}
get version(): number {
return this.model.getVersionId();
}
/**
* Return selected text by Range or all text by default
*/
getText(range?: Range): string {
if (!range) {
return this.model.getValue();
} else {
return this.model.getValueInRange(this.p2m.asRange(range));
}
}
positionAt(offset: number): Position {
const { lineNumber, column } = this.model.getPositionAt(offset);
return this.m2p.asPosition(lineNumber, column);
}
offsetAt(position: Position): number {
return this.model.getOffsetAt(this.p2m.asPosition(position));
}
get lineCount(): number {
return this.model.getLineCount();
}
/**
* Retrieves a line in a text document expressed as a one-based position.
*/
getLineContent(lineNumber: number): string {
return this.model.getLineContent(lineNumber);
}
getLineMaxColumn(lineNumber: number): number {
return this.model.getLineMaxColumn(lineNumber);
}
toValidPosition(position: Position): Position {
const { lineNumber, column } = this.model.validatePosition(this.p2m.asPosition(position));
return this.m2p.asPosition(lineNumber, column);
}
toValidRange(range: Range): Range {
return this.m2p.asRange(this.model.validateRange(this.p2m.asRange(range)));
}
get readOnly(): boolean | MarkdownString {
return this.resource.readOnly ?? false;
}
isReadonly(): boolean | MarkdownString {
return this.readOnly;
}
get onDispose(): monaco.IEvent<void> {
return this.toDispose.onDispose;
}
get onWillDispose(): Event<void> {
return this.toDispose.onDispose;
}
// We have a TypeScript problem here. There is a const enum `DefaultEndOfLine` used for ITextModel and a non-const redeclaration of that enum in the public API in
// Monaco.editor. The values will be the same, but TS won't accept that the two enums are equivalent, so it says these types are irreconcilable.
get textEditorModel(): monaco.editor.ITextModel & ITextModel {
// @ts-expect-error ts(2322)
return this.model;
}
/**
* Find all matches in an editor for the given options.
* @param options the options for finding matches.
*
* @returns the list of matches.
*/
findMatches(options: FindMatchesOptions): FindMatch[] {
const wordSeparators = this.editorPreferences?.['editor.wordSeparators'] ?? editorGeneratedPreferenceProperties['editor.wordSeparators'].default as string;
const results: monaco.editor.FindMatch[] = this.model.findMatches(
options.searchString,
false,
options.isRegex,
options.matchCase,
// eslint-disable-next-line no-null/no-null
options.matchWholeWord ? wordSeparators : null,
true,
options.limitResultCount
);
const extractedMatches: FindMatch[] = [];
results.forEach(r => {
if (r.matches) {
extractedMatches.push({
matches: r.matches,
range: Range.create(r.range.startLineNumber, r.range.startColumn, r.range.endLineNumber, r.range.endColumn)
});
}
});
return extractedMatches;
}
async load(): Promise<MonacoEditorModel> {
await this.resolveModel;
return this;
}
save(options?: SaveOptions): Promise<void> {
return this.scheduleSave(undefined, undefined, {
saveReason: TextDocumentSaveReason.Manual,
...options
});
}
protected pendingOperation = Promise.resolve();
protected async run(operation: () => Promise<void>): Promise<void> {
if (this.toDispose.disposed) {
return;
}
return this.pendingOperation = this.pendingOperation.then(async () => {
try {
await operation();
} catch (e) {
console.error(e);
}
});
}
protected syncCancellationTokenSource = new CancellationTokenSource();
protected cancelSync(): CancellationToken {
this.trace(log => log('MonacoEditorModel.cancelSync'));
this.syncCancellationTokenSource.cancel();
this.syncCancellationTokenSource = new CancellationTokenSource();
return this.syncCancellationTokenSource.token;
}
async sync(): Promise<void> {
const token = this.cancelSync();
return this.run(() => this.doSync(token));
}
protected async doSync(token: CancellationToken): Promise<void> {
this.trace(log => log('MonacoEditorModel.doSync - enter'));
if (token.isCancellationRequested) {
this.trace(log => log('MonacoEditorModel.doSync - exit - cancelled'));
return;
}
const value = await this.readContents();
if (value === undefined) {
this.trace(log => log('MonacoEditorModel.doSync - exit - resource not found'));
return;
}
if (token.isCancellationRequested) {
this.trace(log => log('MonacoEditorModel.doSync - exit - cancelled while looking for a resource'));
return;
}
if (this._dirty) {
this.trace(log => log('MonacoEditorModel.doSync - exit - pending dirty changes'));
return;
}
this.resourceVersion = this.resource.version;
this.updateModel(() => StandaloneServices.get(IModelService).updateModel(this.model, value), {
ignoreDirty: true,
ignoreContentChanges: true
});
this.trace(log => log('MonacoEditorModel.doSync - exit'));
}
protected async readContents(): Promise<string | ITextBufferFactory | undefined> {
try {
const options = { encoding: this.getEncoding() };
const content = await (this.resource.readStream ? this.resource.readStream(options) : this.resource.readContents(options));
let value;
if (typeof content === 'string') {
value = content;
} else {
value = createTextBufferFactoryFromStream(content);
}
this.updateContentEncoding();
this.setValid(true);
return value;
} catch (e) {
this.setValid(false);
if (ResourceError.NotFound.is(e)) {
return undefined;
}
throw e;
}
}
protected ignoreDirtyEdits = false;
protected markAsDirty(): void {
this.trace(log => log('MonacoEditorModel.markAsDirty - enter'));
if (this.ignoreDirtyEdits) {
this.trace(log => log('MonacoEditorModel.markAsDirty - exit - ignoring dirty changes enabled'));
return;
}
this.cancelSync();
this.setDirty(true);
this.trace(log => log('MonacoEditorModel.markAsDirty - exit'));
}
protected saveCancellationTokenSource = new CancellationTokenSource();
protected cancelSave(): CancellationToken {
this.trace(log => log('MonacoEditorModel.cancelSave'));
this.saveCancellationTokenSource.cancel();
this.saveCancellationTokenSource = new CancellationTokenSource();
return this.saveCancellationTokenSource.token;
}
protected scheduleSave(token: CancellationToken = this.cancelSave(), overwriteEncoding?: boolean, options?: SaveOptions): Promise<void> {
return this.run(() => this.doSave(token, overwriteEncoding, options));
}
protected ignoreContentChanges = false;
protected readonly contentChanges: MonacoTextDocumentContentChange[] = [];
protected pushContentChanges(contentChanges: MonacoTextDocumentContentChange[]): void {
if (!this.ignoreContentChanges) {
this.contentChanges.push(...contentChanges);
}
}
protected fireDidChangeContent(event: monaco.editor.IModelContentChangedEvent): void {
this.trace(log => log(`MonacoEditorModel.fireDidChangeContent - enter - ${JSON.stringify(event, undefined, 2)}`));
if (this.model.getAlternativeVersionId() === this.bufferSavedVersionId) {
this.setDirty(false);
} else {
this.markAsDirty();
}
const changeContentEvent = this.asContentChangedEvent(event);
this.onDidChangeContentEmitter.fire(changeContentEvent);
this.pushContentChanges(changeContentEvent.contentChanges);
this.trace(log => log('MonacoEditorModel.fireDidChangeContent - exit'));
}
protected asContentChangedEvent(event: monaco.editor.IModelContentChangedEvent): MonacoModelContentChangedEvent {
const contentChanges = event.changes.map(change => this.asTextDocumentContentChangeEvent(change));
return { model: this, contentChanges };
}
protected asTextDocumentContentChangeEvent(change: monaco.editor.IModelContentChange): MonacoTextDocumentContentChange {
const range = this.m2p.asRange(change.range);
const rangeOffset = change.rangeOffset;
const rangeLength = change.rangeLength;
const text = change.text;
return { range, rangeOffset, rangeLength, text };
}
protected applyEdits(
operations: monaco.editor.IIdentifiedSingleEditOperation[],
options?: Partial<MonacoEditorModel.ApplyEditsOptions>
): void {
return this.updateModel(() => this.model.applyEdits(operations), options);
}
protected updateModel<T>(doUpdate: () => T, options?: Partial<MonacoEditorModel.ApplyEditsOptions>): T {
const resolvedOptions: MonacoEditorModel.ApplyEditsOptions = {
ignoreDirty: false,
ignoreContentChanges: false,
...options
};
const { ignoreDirtyEdits, ignoreContentChanges } = this;
this.ignoreDirtyEdits = resolvedOptions.ignoreDirty;
this.ignoreContentChanges = resolvedOptions.ignoreContentChanges;
try {
return doUpdate();
} finally {
this.ignoreDirtyEdits = ignoreDirtyEdits;
this.ignoreContentChanges = ignoreContentChanges;
}
}
protected async doSave(token: CancellationToken, overwriteEncoding?: boolean, options?: SaveOptions): Promise<void> {
if (token.isCancellationRequested || !this.resource.saveContents) {
return;
}
await this.fireWillSaveModel(token, options);
if (token.isCancellationRequested) {
return;
}
const changes = [...this.contentChanges];
if ((changes.length === 0 && !this.resource.initiallyDirty) && !overwriteEncoding && options?.saveReason !== TextDocumentSaveReason.Manual) {
return;
}
const currentToSaveVersion = this.model.getAlternativeVersionId();
const contentLength = this.model.getValueLength();
const content = this.model.getValue();
try {
const encoding = this.getEncoding();
const version = this.resourceVersion;
await Resource.save(this.resource, { changes, content, contentLength, options: { encoding, overwriteEncoding, version } }, token);
this.contentChanges.splice(0, changes.length);
this.resourceVersion = this.resource.version;
this.updateContentEncoding();
this.setValid(true);
if (token.isCancellationRequested && this.model.getAlternativeVersionId() !== currentToSaveVersion) {
return;
}
this.setDirty(false);
this.fireDidSaveModel();
} catch (e) {
if (!ResourceError.OutOfSync.is(e)) {
throw e;
}
}
}
protected async fireWillSaveModel(token: CancellationToken, options?: SaveOptions): Promise<void> {
await Listener.awaitAll({ model: this, token, options }, this.onWillSaveModelListeners);
}
protected fireDidSaveModel(): void {
this.onDidSaveModelEmitter.fire(this.model);
}
async revert(options?: Saveable.RevertOptions): Promise<void> {
this.trace(log => log('MonacoEditorModel.revert - enter'));
this.cancelSave();
const soft = options && options.soft;
if (soft !== true) {
const dirty = this._dirty;
this._dirty = false;
try {
await this.sync();
} finally {
this._dirty = dirty;
}
}
this.setDirty(false);
this.trace(log => log('MonacoEditorModel.revert - exit'));
}
createSnapshot(preserveBOM?: boolean): ITextSnapshot {
return { read: () => this.model.getValue(undefined, preserveBOM) };
}
applySnapshot(snapshot: Saveable.Snapshot): void {
const value = Saveable.Snapshot.read(snapshot) ?? '';
this.model.setValue(value);
}
async serialize(): Promise<BinaryBuffer> {
return BinaryBuffer.fromString(this.model.getValue());
}
filters(): { [name: string]: string[] } {
const language = monaco.languages.getLanguages().find(lang => lang.id === this.languageId);
if (!language || !language.extensions) {
return {};
}
const name = language.aliases?.[0] || this.languageId;
const extensions = language.extensions.map(ext => ext.startsWith('.') ? ext.substring(1) : ext);
return { [name]: extensions };
}
protected trace(loggable: Loggable): void {
if (this.logger) {
this.logger.debug((log: Log) =>
loggable((message, ...params) => log(message, ...params, this.resource.uri.toString(true)))
);
}
}
}
export namespace MonacoEditorModel {
export interface ApplyEditsOptions {
ignoreDirty: boolean
ignoreContentChanges: boolean
}
}

View File

@@ -0,0 +1,69 @@
// *****************************************************************************
// Copyright (C) 2025 1C-Soft LLC and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { Disposable, DisposableCollection, Emitter } from '@theia/core';
import { DISABLED_CLASS, onDomEvent } from '@theia/core/lib/browser';
import * as monaco from '@theia/monaco-editor-core';
import { MonacoEditor } from './monaco-editor';
export class MonacoEditorOverlayButton implements Disposable {
private static nextId = 1;
readonly domNode: HTMLElement;
protected readonly onClickEmitter = new Emitter<void>();
readonly onClick = this.onClickEmitter.event;
protected readonly toDispose = new DisposableCollection(this.onClickEmitter);
constructor(
editor: MonacoEditor,
label: string,
id = 'theia-editor.overlayButtonWidget' + MonacoEditorOverlayButton.nextId++
) {
this.domNode = document.createElement('div');
this.domNode.classList.add('overlay-button');
this.domNode.textContent = label;
this.toDispose.push(onDomEvent(this.domNode, 'click', () => this.onClickEmitter.fire()));
const overlayWidget: monaco.editor.IOverlayWidget = {
getId: () => id,
getDomNode: () => this.domNode,
getPosition: () => ({
preference: monaco.editor.OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER
})
};
editor.getControl().addOverlayWidget(overlayWidget);
this.toDispose.push(Disposable.create(() => editor.getControl().removeOverlayWidget(overlayWidget)));
}
get enabled(): boolean {
return !this.domNode.classList.contains(DISABLED_CLASS);
}
set enabled(value: boolean) {
if (value) {
this.domNode.classList.remove(DISABLED_CLASS);
} else {
this.domNode.classList.add(DISABLED_CLASS);
}
}
dispose(): void {
this.toDispose.dispose();
}
}

View File

@@ -0,0 +1,285 @@
// *****************************************************************************
// Copyright (C) 2024 1C-Soft LLC and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { Position, Range } from '@theia/core/shared/vscode-languageserver-protocol';
import { DisposableCollection } from '@theia/core';
import { MonacoEditor } from './monaco-editor';
import * as monaco from '@theia/monaco-editor-core';
import { PeekViewWidget, IPeekViewOptions, IPeekViewStyles } from '@theia/monaco-editor-core/esm/vs/editor/contrib/peekView/browser/peekView';
import { ICodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorBrowser';
import { ActionBar } from '@theia/monaco-editor-core/esm/vs/base/browser/ui/actionbar/actionbar';
import { Action } from '@theia/monaco-editor-core/esm/vs/base/common/actions';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
import { IInstantiationService } from '@theia/monaco-editor-core/esm/vs/platform/instantiation/common/instantiation';
import { IThemeService } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/themeService';
import { Color } from '@theia/monaco-editor-core/esm/vs/base/common/color';
export { peekViewBorder, peekViewTitleBackground, peekViewTitleForeground, peekViewTitleInfoForeground }
from '@theia/monaco-editor-core/esm/vs/editor/contrib/peekView/browser/peekView';
export namespace MonacoEditorPeekViewWidget {
export interface Styles {
frameColor?: string;
arrowColor?: string;
headerBackgroundColor?: string;
primaryHeadingColor?: string;
secondaryHeadingColor?: string;
}
export interface Options {
showFrame?: boolean;
showArrow?: boolean;
frameWidth?: number;
className?: string;
isAccessible?: boolean;
isResizeable?: boolean;
keepEditorSelection?: boolean;
allowUnlimitedHeight?: boolean;
ordinal?: number;
showInHiddenAreas?: boolean;
supportOnTitleClick?: boolean;
}
export interface Action {
readonly id: string;
label: string;
tooltip: string;
class: string | undefined;
enabled: boolean;
checked?: boolean;
run(...args: unknown[]): unknown;
}
export interface ActionOptions {
icon?: boolean;
label?: boolean;
keybinding?: string;
index?: number;
}
}
export class MonacoEditorPeekViewWidget {
protected readonly toDispose = new DisposableCollection();
readonly onDidClose = this.toDispose.onDispose;
private readonly themeService = StandaloneServices.get(IThemeService);
private readonly delegate;
constructor(
readonly editor: MonacoEditor,
options: MonacoEditorPeekViewWidget.Options = {},
protected styles: MonacoEditorPeekViewWidget.Styles = {}
) {
const that = this;
this.toDispose.push(this.delegate = new class extends PeekViewWidget {
get actionBar(): ActionBar | undefined {
return this._actionbarWidget;
}
fillContainer(container: HTMLElement): void {
super._fillContainer(container);
}
protected override _fillContainer(container: HTMLElement): void {
that.fillContainer(container);
}
fillHead(container: HTMLElement, noCloseAction?: boolean): void {
super._fillHead(container, noCloseAction);
}
protected override _fillHead(container: HTMLElement, noCloseAction?: boolean): void {
that.fillHead(container, noCloseAction);
}
fillBody(container: HTMLElement): void {
// super._fillBody is an abstract method
}
protected override _fillBody(container: HTMLElement): void {
that.fillBody(container);
};
doLayoutHead(heightInPixel: number, widthInPixel: number): void {
super._doLayoutHead(heightInPixel, widthInPixel);
}
protected override _doLayoutHead(heightInPixel: number, widthInPixel: number): void {
that.doLayoutHead(heightInPixel, widthInPixel);
}
doLayoutBody(heightInPixel: number, widthInPixel: number): void {
super._doLayoutBody(heightInPixel, widthInPixel);
}
protected override _doLayoutBody(heightInPixel: number, widthInPixel: number): void {
that.doLayoutBody(heightInPixel, widthInPixel);
}
onWidth(widthInPixel: number): void {
super._onWidth(widthInPixel);
}
protected override _onWidth(widthInPixel: number): void {
that.onWidth(widthInPixel);
}
doRevealRange(range: monaco.Range, isLastLine: boolean): void {
super.revealRange(range, isLastLine);
}
protected override revealRange(range: monaco.Range, isLastLine: boolean): void {
that.doRevealRange(that.editor['m2p'].asRange(range), isLastLine);
}
getBodyElement(): HTMLDivElement | undefined {
return this._bodyElement;
}
setBodyElement(element: HTMLDivElement | undefined): void {
this._bodyElement = element;
}
getHeadElement(): HTMLDivElement | undefined {
return this._headElement;
}
setHeadElement(element: HTMLDivElement | undefined): void {
this._headElement = element;
}
override setCssClass(className: string, classToReplace?: string | undefined): void {
super.setCssClass(className, classToReplace);
}
}(
editor.getControl() as unknown as ICodeEditor,
Object.assign(<IPeekViewOptions>{}, options, this.convertStyles(styles)),
StandaloneServices.get(IInstantiationService)
));
this.toDispose.push(this.themeService.onDidColorThemeChange(() => this.style(this.styles)));
}
dispose(): void {
this.toDispose.dispose();
}
create(): void {
this.delegate.create();
}
setTitle(primaryHeading: string, secondaryHeading?: string): void {
this.delegate.setTitle(primaryHeading, secondaryHeading);
}
style(styles: MonacoEditorPeekViewWidget.Styles): void {
this.delegate.style(this.convertStyles(this.styles = styles));
}
show(rangeOrPos: Range | Position, heightInLines: number): void {
this.delegate.show(this.convertRangeOrPosition(rangeOrPos), heightInLines);
}
hide(): void {
this.delegate.hide();
}
clearActions(): void {
this.delegate.actionBar?.clear();
}
addAction(id: string, label: string, cssClass: string | undefined, enabled: boolean, actionCallback: (arg: unknown) => unknown,
options?: MonacoEditorPeekViewWidget.ActionOptions): MonacoEditorPeekViewWidget.Action {
options = cssClass ? { icon: true, label: false, ...options } : { icon: false, label: true, ...options };
const { actionBar } = this.delegate;
if (!actionBar) {
throw new Error('Action bar has not been created.');
}
const action = new Action(id, label, cssClass, enabled, actionCallback);
actionBar.push(action, options);
return action;
}
protected fillContainer(container: HTMLElement): void {
this.delegate.fillContainer(container);
}
protected fillHead(container: HTMLElement, noCloseAction?: boolean): void {
this.delegate.fillHead(container, noCloseAction);
}
protected fillBody(container: HTMLElement): void {
this.delegate.fillBody(container);
}
protected doLayoutHead(heightInPixel: number, widthInPixel: number): void {
this.delegate.doLayoutHead(heightInPixel, widthInPixel);
}
protected doLayoutBody(heightInPixel: number, widthInPixel: number): void {
this.delegate.doLayoutBody(heightInPixel, widthInPixel);
}
protected onWidth(widthInPixel: number): void {
this.delegate.onWidth(widthInPixel);
}
protected doRevealRange(range: Range, isLastLine: boolean): void {
this.delegate.doRevealRange(this.editor['p2m'].asRange(range), isLastLine);
}
protected get bodyElement(): HTMLDivElement | undefined {
return this.delegate.getBodyElement();
}
protected set bodyElement(element: HTMLDivElement | undefined) {
this.delegate.setBodyElement(element);
}
protected get headElement(): HTMLDivElement | undefined {
return this.delegate.getHeadElement();
}
protected set headElement(element: HTMLDivElement | undefined) {
this.delegate.setHeadElement(element);
}
protected setCssClass(className: string, classToReplace?: string | undefined): void {
this.delegate.setCssClass(className, classToReplace);
}
private convertStyles(styles: MonacoEditorPeekViewWidget.Styles): IPeekViewStyles {
return {
frameColor: this.convertColor(styles.frameColor),
arrowColor: this.convertColor(styles.arrowColor),
headerBackgroundColor: this.convertColor(styles.headerBackgroundColor),
primaryHeadingColor: this.convertColor(styles.primaryHeadingColor),
secondaryHeadingColor: this.convertColor(styles.secondaryHeadingColor),
};
}
private convertColor(color?: string): Color | undefined {
if (color === undefined) {
return undefined;
}
return this.themeService.getColorTheme().getColor(color) || Color.fromHex(color);
}
private convertRangeOrPosition(arg: Range | Position): monaco.Range | monaco.Position {
const p2m = this.editor['p2m'];
return Range.is(arg) ? p2m.asRange(arg) : p2m.asPosition(arg);
}
}

View File

@@ -0,0 +1,565 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
/* eslint-disable @typescript-eslint/no-explicit-any */
import URI from '@theia/core/lib/common/uri';
import { TextEditor, DiffNavigator } from '@theia/editor/lib/browser';
import { DiffUris } from '@theia/core/lib/browser/diff-uris';
import { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify';
import { DisposableCollection, deepClone, Disposable, CancellationToken } from '@theia/core/lib/common';
import { MonacoDiffEditor } from './monaco-diff-editor';
import { MonacoDiffNavigatorFactory } from './monaco-diff-navigator-factory';
import { EditorServiceOverrides, MonacoEditor, MonacoEditorServices } from './monaco-editor';
import { MonacoEditorModel, TextDocumentSaveReason } from './monaco-editor-model';
import { MonacoWorkspace } from './monaco-workspace';
import { ContributionProvider } from '@theia/core';
import { KeybindingRegistry, OpenerService, open, WidgetOpenerOptions, SaveOptions, FormatType } from '@theia/core/lib/browser';
import { MonacoResolvedKeybinding } from './monaco-resolved-keybinding';
import { HttpOpenHandlerOptions } from '@theia/core/lib/browser/http-open-handler';
import { MonacoToProtocolConverter } from './monaco-to-protocol-converter';
import { ProtocolToMonacoConverter } from './protocol-to-monaco-converter';
import * as monaco from '@theia/monaco-editor-core';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
import { IOpenerService, OpenExternalOptions, OpenInternalOptions } from '@theia/monaco-editor-core/esm/vs/platform/opener/common/opener';
import { IKeybindingService } from '@theia/monaco-editor-core/esm/vs/platform/keybinding/common/keybinding';
import { IContextMenuService } from '@theia/monaco-editor-core/esm/vs/platform/contextview/browser/contextView';
import { KeyCodeChord } from '@theia/monaco-editor-core/esm/vs/base/common/keybindings';
import { IContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/common/contextkey';
import { ITextModelService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/resolverService';
import { IReference } from '@theia/monaco-editor-core/esm/vs/base/common/lifecycle';
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
import { SimpleMonacoEditor } from './simple-monaco-editor';
import { ICodeEditorWidgetOptions } from '@theia/monaco-editor-core/esm/vs/editor/browser/widget/codeEditor/codeEditorWidget';
import { timeoutReject } from '@theia/core/lib/common/promise-util';
import { FileSystemPreferences } from '@theia/filesystem/lib/common';
import { insertFinalNewline } from './monaco-utilities';
import { EditorPreferenceChange, EditorPreferences } from '@theia/editor/lib/common/editor-preferences';
export const MonacoEditorFactory = Symbol('MonacoEditorFactory');
export interface MonacoEditorFactory {
readonly scheme: string;
create(model: MonacoEditorModel, defaultOptions: MonacoEditor.IOptions, defaultOverrides: EditorServiceOverrides): Promise<MonacoEditor>;
}
export const SaveParticipant = Symbol('SaveParticipant');
export interface SaveParticipant {
readonly order: number;
applyChangesOnSave(
editor: MonacoEditor,
cancellationToken: CancellationToken,
options?: SaveOptions): Promise<void>;
}
export const SAVE_PARTICIPANT_DEFAULT_ORDER = 0;
@injectable()
export class MonacoEditorProvider {
@inject(ContributionProvider)
@named(MonacoEditorFactory)
protected readonly factories: ContributionProvider<MonacoEditorFactory>;
@inject(MonacoEditorServices)
protected readonly services: MonacoEditorServices;
@inject(KeybindingRegistry)
protected readonly keybindingRegistry: KeybindingRegistry;
@inject(OpenerService)
protected readonly openerService: OpenerService;
@inject(ContributionProvider)
@named(SaveParticipant)
protected readonly saveProviderContributions: ContributionProvider<SaveParticipant>;
@inject(FileSystemPreferences)
protected readonly filePreferences: FileSystemPreferences;
protected saveParticipants: SaveParticipant[];
protected _current: MonacoEditor | undefined;
/**
* Returns the last focused MonacoEditor.
* It takes into account inline editors as well.
* If you are interested only in standalone editors then use `MonacoEditor.getCurrent(EditorManager)`
*/
get current(): MonacoEditor | undefined {
return this._current;
}
constructor(
@inject(MonacoToProtocolConverter) protected readonly m2p: MonacoToProtocolConverter,
@inject(ProtocolToMonacoConverter) protected readonly p2m: ProtocolToMonacoConverter,
@inject(MonacoWorkspace) protected readonly workspace: MonacoWorkspace,
@inject(EditorPreferences) protected readonly editorPreferences: EditorPreferences,
@inject(MonacoDiffNavigatorFactory) protected readonly diffNavigatorFactory: MonacoDiffNavigatorFactory,
) {
}
protected async getModel(uri: URI, toDispose: DisposableCollection): Promise<MonacoEditorModel> {
const reference = await StandaloneServices.get(ITextModelService).createModelReference(monaco.Uri.from(uri.toComponents())) as IReference<MonacoEditorModel>;
// if document is invalid makes sure that all events from underlying resource are processed before throwing invalid model
if (!reference.object.valid) {
await reference.object.sync();
}
if (!reference.object.valid) {
reference.dispose();
throw Object.assign(new Error(`'${uri.toString()}' is invalid`), { code: 'MODEL_IS_INVALID' });
}
toDispose.push(reference);
return reference.object;
}
async get(uri: URI): Promise<MonacoEditor> {
await this.editorPreferences.ready;
return this.doCreateEditor(uri, (override, toDispose) => this.createEditor(uri, override, toDispose));
}
protected async doCreateEditor<T extends MonacoEditor | SimpleMonacoEditor>(uri: URI, factory: (
override: EditorServiceOverrides, toDispose: DisposableCollection) => Promise<T>
): Promise<T> {
const domNode = document.createElement('div');
const contextKeyService = StandaloneServices.get(IContextKeyService).createScoped(domNode);
StandaloneServices.get(IOpenerService).registerOpener({
open: (u, options) => this.interceptOpen(u, options)
});
const overrides: EditorServiceOverrides = [
[IContextKeyService, contextKeyService],
];
const toDispose = new DisposableCollection();
const editor = await factory(overrides, toDispose);
editor.onDispose(() => toDispose.dispose());
if (editor instanceof MonacoEditor) {
this.injectKeybindingResolver(editor);
toDispose.push(editor.onFocusChanged(focused => {
if (focused) {
this._current = editor;
}
}));
toDispose.push(Disposable.create(() => {
if (this._current === editor) {
this._current = undefined;
}
}));
}
return editor;
}
/**
* Intercept internal Monaco open calls and delegate to OpenerService.
*/
protected async interceptOpen(monacoUri: monaco.Uri | string, monacoOptions?: OpenInternalOptions | OpenExternalOptions): Promise<boolean> {
let options = undefined;
if (monacoOptions) {
if ('openToSide' in monacoOptions && monacoOptions.openToSide) {
options = Object.assign(options || {}, <WidgetOpenerOptions>{
widgetOptions: {
mode: 'split-right'
}
});
}
if ('openExternal' in monacoOptions && monacoOptions.openExternal) {
options = Object.assign(options || {}, <HttpOpenHandlerOptions>{
openExternal: true
});
}
}
const uri = new URI(monacoUri.toString());
try {
await open(this.openerService, uri, options);
return true;
} catch (e) {
console.error(`Fail to open '${uri.toString()}':`, e);
return false;
}
}
protected injectKeybindingResolver(editor: MonacoEditor): void {
const keybindingService = StandaloneServices.get(IKeybindingService);
keybindingService.resolveKeybinding = keybinding => [new MonacoResolvedKeybinding(MonacoResolvedKeybinding.keySequence(keybinding.chords), this.keybindingRegistry)];
keybindingService.resolveKeyboardEvent = keyboardEvent => {
const keybinding = new KeyCodeChord(
keyboardEvent.ctrlKey,
keyboardEvent.shiftKey,
keyboardEvent.altKey,
keyboardEvent.metaKey,
keyboardEvent.keyCode
);
return new MonacoResolvedKeybinding(MonacoResolvedKeybinding.keySequence([keybinding]), this.keybindingRegistry);
};
}
protected createEditor(uri: URI, override: EditorServiceOverrides, toDispose: DisposableCollection): Promise<MonacoEditor> {
if (DiffUris.isDiffUri(uri)) {
return this.createMonacoDiffEditor(uri, override, toDispose);
}
return this.createMonacoEditor(uri, override, toDispose);
}
protected get preferencePrefixes(): string[] {
return ['editor.'];
}
async createMonacoEditor(uri: URI, override: EditorServiceOverrides, toDispose: DisposableCollection): Promise<MonacoEditor> {
const model = await this.getModel(uri, toDispose);
const options = this.createMonacoEditorOptions(model);
const factory = this.factories.getContributions().find(({ scheme }) => uri.scheme === scheme);
const editor = factory
? await factory.create(model, options, override)
: await MonacoEditor.create(uri, model, document.createElement('div'), this.services, options, override);
toDispose.push(this.editorPreferences.onPreferenceChanged(event => {
if (event.affects(uri.toString(), model.languageId)) {
this.updateMonacoEditorOptions(editor, event);
}
}));
toDispose.push(editor.onLanguageChanged(() => this.updateMonacoEditorOptions(editor)));
toDispose.push(editor.onDidChangeReadOnly(() => this.updateReadOnlyMessage(options, model.readOnly)));
toDispose.push(editor.document.onModelWillSaveModel(e => this.runSaveParticipants(editor, e.token, e.options)));
return editor;
}
protected updateReadOnlyMessage(options: MonacoEditor.IOptions, readOnly: boolean | MarkdownString): void {
options.readOnlyMessage = MarkdownString.is(readOnly) ? readOnly : undefined;
}
protected createMonacoEditorOptions(model: MonacoEditorModel): MonacoEditor.IOptions {
const options = this.createOptions(this.preferencePrefixes, model.uri, model.languageId);
// eslint-disable-next-line no-null/no-null
options.model = null; // explicitly set to null to avoid creating an initial model automatically
options.readOnly = model.readOnly;
this.updateReadOnlyMessage(options, model.readOnly);
options.lineNumbersMinChars = model.lineNumbersMinChars;
return options;
}
protected updateMonacoEditorOptions(editor: MonacoEditor, event?: EditorPreferenceChange): void {
if (event) {
const preferenceName = event.preferenceName;
const overrideIdentifier = editor.document.languageId;
const newValue = this.editorPreferences.get({ preferenceName, overrideIdentifier }, undefined, editor.uri.toString());
editor.getControl().updateOptions(this.setOption(preferenceName, newValue, this.preferencePrefixes));
} else {
const options = this.createMonacoEditorOptions(editor.document);
delete options.model;
editor.getControl().updateOptions(options);
}
}
protected get diffPreferencePrefixes(): string[] {
return [...this.preferencePrefixes, 'diffEditor.'];
}
protected async createMonacoDiffEditor(uri: URI, override: EditorServiceOverrides, toDispose: DisposableCollection): Promise<MonacoDiffEditor> {
const [original, modified] = DiffUris.decode(uri);
const [originalModel, modifiedModel] = await Promise.all([this.getModel(original, toDispose), this.getModel(modified, toDispose)]);
const options = this.createMonacoDiffEditorOptions(originalModel, modifiedModel);
const editor = new MonacoDiffEditor(
uri,
document.createElement('div'),
originalModel, modifiedModel,
this.services,
this.diffNavigatorFactory,
options,
override);
toDispose.push(this.editorPreferences.onPreferenceChanged(event => {
const originalFileUri = original.withoutQuery().withScheme('file').toString();
if (event.affects(originalFileUri, editor.document.languageId)) {
this.updateMonacoDiffEditorOptions(editor, event, originalFileUri);
}
}));
toDispose.push(editor.onLanguageChanged(() => this.updateMonacoDiffEditorOptions(editor)));
return editor;
}
protected createMonacoDiffEditorOptions(original: MonacoEditorModel, modified: MonacoEditorModel): MonacoDiffEditor.IOptions {
const options = this.createOptions(this.diffPreferencePrefixes, modified.uri, modified.languageId);
options.originalEditable = !original.readOnly;
options.readOnly = modified.readOnly;
options.readOnlyMessage = MarkdownString.is(modified.readOnly) ? modified.readOnly : undefined;
return options;
}
protected updateMonacoDiffEditorOptions(editor: MonacoDiffEditor, event?: EditorPreferenceChange, resourceUri?: string): void {
if (event) {
const preferenceName = event.preferenceName;
const overrideIdentifier = editor.document.languageId;
const newValue = this.editorPreferences.get({ preferenceName, overrideIdentifier }, undefined, resourceUri);
editor.diffEditor.updateOptions(this.setOption(preferenceName, newValue, this.diffPreferencePrefixes));
} else {
const options = this.createMonacoDiffEditorOptions(editor.originalModel, editor.modifiedModel);
editor.diffEditor.updateOptions(options);
}
}
/** @deprecated always pass a language as an overrideIdentifier */
protected createOptions(prefixes: string[], uri: string): Record<string, any>;
protected createOptions(prefixes: string[], uri: string, overrideIdentifier: string): Record<string, any>;
protected createOptions(prefixes: string[], uri: string, overrideIdentifier?: string): Record<string, any> {
const flat: Record<string, any> = {};
for (const preferenceName of Object.keys(this.editorPreferences)) {
flat[preferenceName] = (<any>this.editorPreferences).get({ preferenceName, overrideIdentifier }, undefined, uri);
}
return Object.entries(flat).reduce((tree, [preferenceName, value]) => this.setOption(preferenceName, deepClone(value), prefixes, tree), {});
}
protected setOption(preferenceName: string, value: any, prefixes: string[], options: Record<string, any> = {}): {
[name: string]: any;
} {
const optionName = this.toOptionName(preferenceName, prefixes);
this.doSetOption(options, value, optionName.split('.'));
return options;
}
protected toOptionName(preferenceName: string, prefixes: string[]): string {
for (const prefix of prefixes) {
if (preferenceName.startsWith(prefix)) {
return preferenceName.substring(prefix.length);
}
}
return preferenceName;
}
protected doSetOption(obj: Record<string, any>, value: any, names: string[]): void {
for (let i = 0; i < names.length - 1; i++) {
const name = names[i];
if (obj[name] === undefined) {
obj = obj[name] = {};
} else if (typeof obj[name] !== 'object' || obj[name] === null) { // eslint-disable-line no-null/no-null
console.warn(`Preference (diff)editor.${names.join('.')} conflicts with another preference name.`);
obj = obj[name] = {};
} else {
obj = obj[name];
}
}
obj[names[names.length - 1]] = value;
}
getDiffNavigator(editor: TextEditor): DiffNavigator {
if (editor instanceof MonacoDiffEditor) {
return editor.diffNavigator;
}
return MonacoDiffNavigatorFactory.nullNavigator;
}
/**
* Creates an instance of the standard MonacoEditor with a StandaloneCodeEditor as its Monaco delegate.
* Among other differences, these editors execute basic actions like typing or deletion via commands that may be overridden by extensions.
* @deprecated Most use cases for inline editors should be served by `createSimpleInline` instead.
*/
async createInline(uri: URI, node: HTMLElement, options?: MonacoEditor.IOptions): Promise<MonacoEditor> {
return this.doCreateEditor(uri, async (override, toDispose) => {
const overrides = override ? Array.from(override) : [];
overrides.push([IContextMenuService, { showContextMenu: () => {/** no op! */ } }]);
const document = await this.getModel(uri, toDispose);
document.suppressOpenEditorWhenDirty = true;
const model = (await document.load()).textEditorModel;
return await MonacoEditor.create(
uri,
document,
node,
this.services,
Object.assign({
model,
autoSizing: false,
minHeight: 1,
maxHeight: 1
}, MonacoEditorProvider.inlineOptions, options),
overrides
);
});
}
/**
* Creates an instance of the standard MonacoEditor with a CodeEditorWidget as its Monaco delegate.
* In addition to the service customizability of the StandaloneCodeEditor,This editor allows greater customization the editor contributions active in the widget.
* See {@link ICodeEditorWidgetOptions.contributions}.
*/
async createSimpleInline(uri: URI, node: HTMLElement, options?: MonacoEditor.IOptions, widgetOptions?: ICodeEditorWidgetOptions): Promise<SimpleMonacoEditor> {
return this.doCreateEditor(uri, async (override, toDispose) => {
const overrides = override ? Array.from(override) : [];
overrides.push([IContextMenuService, { showContextMenu: () => { /** no op! */ } }]);
const document = await this.getModel(uri, toDispose);
document.suppressOpenEditorWhenDirty = true;
const model = (await document.load()).textEditorModel;
const baseOptions: Partial<MonacoEditor.IOptions> = {
model,
autoSizing: false,
minHeight: 1,
maxHeight: 1
};
const editorOptions = {
...baseOptions,
...MonacoEditorProvider.inlineOptions,
...options
};
return new SimpleMonacoEditor(
uri,
document,
node,
this.services,
editorOptions,
overrides,
{ isSimpleWidget: true, ...widgetOptions }
);
});
}
static inlineOptions: monaco.editor.IEditorConstructionOptions = {
wordWrap: 'on',
overviewRulerLanes: 0,
glyphMargin: false,
lineNumbers: 'off',
folding: false,
selectOnLineNumbers: false,
hideCursorInOverviewRuler: true,
selectionHighlight: false,
scrollbar: {
horizontal: 'hidden'
},
lineDecorationsWidth: 0,
overviewRulerBorder: false,
scrollBeyondLastLine: false,
renderLineHighlight: 'none',
fixedOverflowWidgets: true,
acceptSuggestionOnEnter: 'smart',
minimap: {
enabled: false
}
};
async createEmbeddedDiffEditor(parentEditor: MonacoEditor, node: HTMLElement, originalUri: URI, modifiedUri: URI = parentEditor.uri,
options?: MonacoDiffEditor.IOptions): Promise<MonacoDiffEditor> {
options = {
scrollBeyondLastLine: true,
overviewRulerLanes: 2,
fixedOverflowWidgets: true,
minimap: { enabled: false },
renderSideBySide: false,
readOnly: false,
renderIndicators: false,
diffAlgorithm: 'advanced',
stickyScroll: { enabled: false },
...options,
scrollbar: {
verticalScrollbarSize: 14,
horizontal: 'auto',
useShadows: true,
verticalHasArrows: false,
horizontalHasArrows: false,
...options?.scrollbar
}
};
const uri = DiffUris.encode(originalUri, modifiedUri);
return await this.doCreateEditor(uri, async (override, toDispose) =>
new MonacoDiffEditor(
uri,
node,
await this.getModel(originalUri, toDispose),
await this.getModel(modifiedUri, toDispose),
this.services,
this.diffNavigatorFactory,
options,
override,
parentEditor
)
);
}
@postConstruct()
init(): void {
this.saveParticipants = this.saveProviderContributions.getContributions().slice().sort((left, right) => left.order - right.order);
this.registerSaveParticipant({
order: 1000,
applyChangesOnSave: (
editor: MonacoEditor,
cancellationToken: monaco.CancellationToken,
options: SaveOptions): Promise<void> => this.formatOnSave(editor, editor.document, cancellationToken, options)
});
}
registerSaveParticipant(saveParticipant: SaveParticipant): Disposable {
if (this.saveParticipants.find(value => value === saveParticipant)) {
throw new Error('Save participant already registered');
}
this.saveParticipants.push(saveParticipant);
this.saveParticipants.sort((left, right) => left.order - right.order);
return Disposable.create(() => {
const index = this.saveParticipants.indexOf(saveParticipant);
if (index >= 0) {
this.saveParticipants.splice(index, 1);
}
});
}
protected shouldFormat(model: MonacoEditorModel, options: SaveOptions): boolean {
if (options.saveReason !== TextDocumentSaveReason.Manual) {
return false;
}
switch (options.formatType) {
case FormatType.ON: return true;
case FormatType.OFF: return false;
case FormatType.DIRTY: return model.dirty;
}
return true;
}
async runSaveParticipants(editor: MonacoEditor, cancellationToken: CancellationToken, options?: SaveOptions): Promise<void> {
const initialState = editor.document.createSnapshot();
for (const participant of this.saveParticipants) {
if (cancellationToken.isCancellationRequested) {
break;
}
const snapshot = editor.document.createSnapshot();
try {
await participant.applyChangesOnSave(editor, cancellationToken, options);
} catch (e) {
console.error(e);
editor.document.applySnapshot(snapshot);
}
}
if (cancellationToken.isCancellationRequested) {
editor.document.applySnapshot(initialState);
}
}
protected async formatOnSave(
editor: MonacoEditor,
model: MonacoEditorModel,
cancellationToken: CancellationToken,
options: SaveOptions): Promise<void> {
if (!this.shouldFormat(model, options)) {
return;
}
const overrideIdentifier = model.languageId;
const uri = model.uri.toString();
const formatOnSave = this.editorPreferences.get({ preferenceName: 'editor.formatOnSave', overrideIdentifier }, undefined, uri);
if (formatOnSave) {
const formatOnSaveTimeout = this.editorPreferences.get({ preferenceName: 'editor.formatOnSaveTimeout', overrideIdentifier }, undefined, uri)!;
await Promise.race([
timeoutReject(formatOnSaveTimeout, `Aborted format on save after ${formatOnSaveTimeout}ms`),
await editor.runAction('editor.action.formatDocument')
]);
}
const shouldRemoveWhiteSpace = this.filePreferences.get({ preferenceName: 'files.trimTrailingWhitespace', overrideIdentifier }, undefined, uri);
if (shouldRemoveWhiteSpace) {
await editor.runAction('editor.action.trimTrailingWhitespace');
}
const shouldInsertFinalNewline = this.filePreferences.get({ preferenceName: 'files.insertFinalNewline', overrideIdentifier }, undefined, uri);
if (shouldInsertFinalNewline) {
this.insertFinalNewline(model);
}
}
protected insertFinalNewline(editorModel: MonacoEditorModel): void {
insertFinalNewline(editorModel);
}
}

View File

@@ -0,0 +1,169 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject, decorate, named } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { OpenerService, open, WidgetOpenMode, ApplicationShell } from '@theia/core/lib/browser';
import { EditorWidget, EditorOpenerOptions, EditorManager, CustomEditorWidget } from '@theia/editor/lib/browser';
import { MonacoEditor } from './monaco-editor';
import { MonacoToProtocolConverter } from './monaco-to-protocol-converter';
import { MonacoEditorModel } from './monaco-editor-model';
import { IResourceEditorInput, ITextResourceEditorInput } from '@theia/monaco-editor-core/esm/vs/platform/editor/common/editor';
import { StandaloneCodeEditorService } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneCodeEditorService';
import { StandaloneCodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneCodeEditor';
import { ICodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorBrowser';
import { IContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/common/contextkey';
import { IThemeService } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/themeService';
import { ContributionProvider, PreferenceService } from '@theia/core';
decorate(injectable(), StandaloneCodeEditorService);
export const VSCodeContextKeyService = Symbol('VSCodeContextKeyService');
export const VSCodeThemeService = Symbol('VSCodeThemeService');
export const MonacoEditorServiceFactory = Symbol('MonacoEditorServiceFactory');
export type MonacoEditorServiceFactoryType = (contextKeyService: IContextKeyService, themeService: IThemeService) => MonacoEditorService;
/**
* contribution provider to extend the active editor handling to other editor types than just standalone editor widgets.
*/
export const ActiveMonacoEditorContribution = Symbol('ActiveMonacoEditorContribution');
export interface ActiveMonacoEditorContribution {
getActiveEditor(): ICodeEditor | undefined;
}
@injectable()
export class MonacoEditorService extends StandaloneCodeEditorService {
public static readonly ENABLE_PREVIEW_PREFERENCE: string = 'editor.enablePreview';
@inject(OpenerService)
protected readonly openerService: OpenerService;
@inject(MonacoToProtocolConverter)
protected readonly m2p: MonacoToProtocolConverter;
@inject(ApplicationShell)
protected readonly shell: ApplicationShell;
@inject(EditorManager)
protected readonly editors: EditorManager;
@inject(PreferenceService)
protected readonly preferencesService: PreferenceService;
@inject(ContributionProvider) @named(ActiveMonacoEditorContribution)
protected readonly activeMonacoEditorContribution: ContributionProvider<ActiveMonacoEditorContribution>;
constructor(@inject(VSCodeContextKeyService) contextKeyService: IContextKeyService, @inject(VSCodeThemeService) themeService: IThemeService) {
super(contextKeyService, themeService);
}
/**
* Monaco active editor is either focused or last focused editor.
*/
override getActiveCodeEditor(): ICodeEditor | null {
let editor = MonacoEditor.getCurrent(this.editors);
if (!editor && CustomEditorWidget.is(this.shell.activeWidget)) {
const model = this.shell.activeWidget.modelRef.object;
if (model?.editorTextModel instanceof MonacoEditorModel) {
editor = MonacoEditor.findByDocument(this.editors, model.editorTextModel)[0];
}
}
const candidate = editor?.getControl();
// Since we extend a private super class, we have to check that the thing that matches the public interface also matches the private expectations the superclass.
if (candidate instanceof StandaloneCodeEditor) {
return candidate;
}
for (const activeEditorProvider of this.activeMonacoEditorContribution.getContributions()) {
const activeEditor = activeEditorProvider.getActiveEditor();
if (activeEditor) {
return activeEditor;
}
}
/* eslint-disable-next-line no-null/no-null */
return null;
}
override async openCodeEditor(input: IResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise<ICodeEditor | null> {
const uri = new URI(input.resource.toString());
const openerOptions = this.createEditorOpenerOptions(input, source, sideBySide);
const widget = await open(this.openerService, uri, openerOptions);
const editorWidget = await this.findEditorWidgetByUri(widget, uri.toString());
const candidate = MonacoEditor.get(editorWidget)?.getControl();
// Since we extend a private super class, we have to check that the thing that matches the public interface also matches the private expectations the superclass.
// eslint-disable-next-line no-null/no-null
return candidate instanceof StandaloneCodeEditor ? candidate : null;
}
protected async findEditorWidgetByUri(widget: object | undefined, uriAsString: string): Promise<EditorWidget | undefined> {
if (widget instanceof EditorWidget) {
if (widget.editor.uri.toString() === uriAsString) {
return widget;
}
return undefined;
}
if (ApplicationShell.TrackableWidgetProvider.is(widget)) {
for (const childWidget of widget.getTrackableWidgets()) {
const editorWidget = await this.findEditorWidgetByUri(childWidget, uriAsString);
if (editorWidget) {
return editorWidget;
}
}
}
return undefined;
}
protected createEditorOpenerOptions(input: IResourceEditorInput | ITextResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): EditorOpenerOptions {
const mode = this.getEditorOpenMode(input);
const widgetOptions = this.getWidgetOptions(source, sideBySide);
const selection = this.getSelection(input);
const preview = !!this.preferencesService.get<boolean>(MonacoEditorService.ENABLE_PREVIEW_PREFERENCE, false);
return { mode, widgetOptions, preview, selection };
}
protected getSelection(input: IResourceEditorInput | ITextResourceEditorInput): EditorOpenerOptions['selection'] {
if ('options' in input && input.options && 'selection' in input.options) {
return this.m2p.asRange(input.options.selection);
}
}
protected getEditorOpenMode(input: IResourceEditorInput): WidgetOpenMode {
const options = {
preserveFocus: false,
revealIfVisible: true,
...input.options
};
if (options.preserveFocus) {
return 'reveal';
}
return options.revealIfVisible ? 'activate' : 'open';
}
protected getWidgetOptions(source: ICodeEditor | null, sideBySide?: boolean): ApplicationShell.WidgetOptions | undefined {
const ref = MonacoEditor.getWidgetFor(this.editors, source);
if (!ref) {
return undefined;
}
const area = (ref && this.shell.getAreaFor(ref)) || 'main';
const mode = ref && sideBySide ? 'split-right' : undefined;
if (area === 'secondaryWindow') {
return { area: 'main', mode };
}
return { area, mode, ref };
}
}

View File

@@ -0,0 +1,250 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation and others. All rights reserved.
* Licensed under the MIT License. See https://github.com/Microsoft/vscode/blob/master/LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, DisposableCollection, Event, Emitter } from '@theia/core';
import { TrackedRangeStickiness } from '@theia/editor/lib/browser';
import * as monaco from '@theia/monaco-editor-core';
export interface MonacoEditorViewZone extends monaco.editor.IViewZone {
id: string;
}
export class MonacoEditorZoneWidget implements Disposable {
private arrow: Arrow | undefined;
readonly zoneNode = document.createElement('div');
readonly containerNode = document.createElement('div');
protected readonly onDidLayoutChangeEmitter = new Emitter<monaco.editor.IDimension>();
readonly onDidLayoutChange: Event<monaco.editor.IDimension> = this.onDidLayoutChangeEmitter.event;
protected viewZone: MonacoEditorViewZone | undefined;
protected readonly toHide = new DisposableCollection();
protected readonly toDispose = new DisposableCollection(
this.onDidLayoutChangeEmitter,
this.toHide
);
editor: monaco.editor.ICodeEditor;
constructor(
editorInstance: monaco.editor.ICodeEditor, readonly showArrow: boolean = true
) {
this.editor = editorInstance;
this.zoneNode.classList.add('zone-widget');
this.containerNode.classList.add('zone-widget-container');
this.zoneNode.appendChild(this.containerNode);
this.updateWidth();
this.toDispose.push(this.editor.onDidLayoutChange(info => this.updateWidth(info)));
}
dispose(): void {
this.toDispose.dispose();
this.hide();
}
protected _options: MonacoEditorZoneWidget.Options | undefined;
get options(): MonacoEditorZoneWidget.Options | undefined {
return this.viewZone ? this._options : undefined;
}
hide(): void {
this.toHide.dispose();
}
show(options: MonacoEditorZoneWidget.Options): void {
let { afterLineNumber, afterColumn, heightInLines } = this._options = { showFrame: true, ...options };
const lineHeight = this.editor.getOption(monaco.editor.EditorOption.lineHeight);
// adjust heightInLines to viewport
const maxHeightInLines = Math.max(12, (this.editor.getLayoutInfo().height / lineHeight) * 0.8);
heightInLines = Math.min(heightInLines, maxHeightInLines);
let arrowHeight = 0;
this.toHide.dispose();
this.editor.changeViewZones(accessor => {
this.zoneNode.style.top = '-1000px';
const domNode = document.createElement('div');
domNode.style.overflow = 'hidden';
const zone: monaco.editor.IViewZone = {
domNode,
afterLineNumber,
afterColumn,
heightInLines,
onDomNodeTop: zoneTop => this.updateTop(zoneTop),
onComputedHeight: zoneHeight => this.updateHeight(zoneHeight)
};
this.viewZone = Object.assign(zone, {
id: accessor.addZone(zone)
});
const id = this.viewZone.id;
this.toHide.push(Disposable.create(() => {
this.editor.changeViewZones(a => a.removeZone(id));
this.viewZone = undefined;
}));
if (this.showArrow) {
this.arrow = new Arrow(this.editor);
arrowHeight = Math.round(lineHeight / 3);
this.arrow.height = arrowHeight;
this.arrow.show({ lineNumber: options.afterLineNumber, column: 0 });
this.toHide.push(this.arrow);
}
const widget: monaco.editor.IOverlayWidget = {
getId: () => 'editor-zone-widget-' + id,
getDomNode: () => this.zoneNode,
// eslint-disable-next-line no-null/no-null
getPosition: () => null!
};
this.editor.addOverlayWidget(widget);
this.toHide.push(Disposable.create(() => this.editor.removeOverlayWidget(widget)));
});
this.containerNode.style.overflow = 'hidden';
this.updateContainerHeight(heightInLines * lineHeight);
const model = this.editor.getModel();
if (model) {
const revealLineNumber = Math.min(model.getLineCount(), Math.max(1, afterLineNumber + 1));
this.editor.revealLine(revealLineNumber, monaco.editor.ScrollType.Smooth);
}
}
layout(heightInLines: number): void {
if (this.viewZone && this.viewZone.heightInLines !== heightInLines) {
this.viewZone.heightInLines = heightInLines;
const id = this.viewZone.id;
this.editor.changeViewZones(accessor => accessor.layoutZone(id));
}
}
protected updateTop(top: number): void {
this.zoneNode.style.top = top + (this.showArrow ? 6 : 0) + 'px';
}
protected updateHeight(zoneHeight: number): void {
this.zoneNode.style.height = zoneHeight + 'px';
this.updateContainerHeight(zoneHeight);
}
protected updateContainerHeight(zoneHeight: number): void {
const { frameWidth, height } = this.computeContainerHeight(zoneHeight);
this.containerNode.style.height = height + 'px';
this.containerNode.style.borderTopWidth = frameWidth + 'px';
this.containerNode.style.borderBottomWidth = frameWidth + 'px';
const width = this.computeWidth();
this.onDidLayoutChangeEmitter.fire({ height, width });
}
protected computeContainerHeight(zoneHeight: number): {
height: number,
frameWidth: number
} {
const lineHeight = this.editor.getOption(monaco.editor.EditorOption.lineHeight);
const frameWidth = this._options && this._options.frameWidth;
const frameThickness = this._options && this._options.showFrame ? Math.round(lineHeight / 9) : 0;
return {
frameWidth: frameWidth !== undefined ? frameWidth : frameThickness,
height: zoneHeight - 2 * frameThickness
};
}
protected updateWidth(info: monaco.editor.EditorLayoutInfo = this.editor.getLayoutInfo()): void {
const width = this.computeWidth(info);
this.zoneNode.style.width = width + 'px';
this.zoneNode.style.left = this.computeLeft(info) + 'px';
}
protected computeWidth(info: monaco.editor.EditorLayoutInfo = this.editor.getLayoutInfo()): number {
return info.width - info.minimap.minimapWidth - info.verticalScrollbarWidth;
}
protected computeLeft(info: monaco.editor.EditorLayoutInfo = this.editor.getLayoutInfo()): number {
// If minimap is to the left, we move beyond it
if (info.minimap.minimapWidth > 0 && info.minimap.minimapLeft === 0) {
return info.minimap.minimapWidth;
}
return 0;
}
}
class IdGenerator {
private lastId: number;
constructor(private prefix: string) {
this.lastId = 0;
}
nextId(): string {
return this.prefix + (++this.lastId);
}
}
class Arrow implements Disposable {
private readonly idGenerator = new IdGenerator('.arrow-decoration-');
private readonly ruleName = this.idGenerator.nextId();
private decorations: string[] = [];
private _height: number = -1;
constructor(
private readonly _editor: monaco.editor.ICodeEditor
) { }
dispose(): void {
this.hide();
}
set height(value: number) {
if (this._height !== value) {
this._height = value;
this._updateStyle();
}
}
private _updateStyle(): void {
const style = document.createElement('style');
style.type = 'text/css';
style.media = 'screen';
document.getElementsByTagName('head')[0].appendChild(style);
const selector = `.monaco-editor ${this.ruleName}`;
const cssText = `border-style: solid; border-color: transparent transparent var(--theia-peekView-border); border-width:
${this._height}px; bottom: -${this._height}px; margin-left: -${this._height}px; `;
(<CSSStyleSheet>style.sheet).insertRule(selector + '{' + cssText + '}', 0);
}
show(where: monaco.IPosition): void {
this.decorations = this._editor.deltaDecorations(
this.decorations,
[{ range: monaco.Range.fromPositions(where), options: { className: this.ruleName, stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } }]
);
}
hide(): void {
this._editor.deltaDecorations(this.decorations, []);
}
}
export namespace MonacoEditorZoneWidget {
export interface Options {
afterLineNumber: number,
afterColumn?: number,
heightInLines: number,
showFrame?: boolean,
frameWidth?: number
}
}

View File

@@ -0,0 +1,845 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject, unmanaged } from '@theia/core/shared/inversify';
import { ElementExt } from '@theia/core/shared/@lumino/domutils';
import URI from '@theia/core/lib/common/uri';
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
import { DisposableCollection, Disposable, Emitter, Event, nullToUndefined, MaybeNull } from '@theia/core/lib/common';
import {
Dimension,
EditorManager,
EditorWidget,
Position,
Range,
TextDocumentContentChangeDelta,
TextDocumentChangeEvent,
TextEditor,
RevealRangeOptions,
RevealPositionOptions,
DeltaDecorationParams,
ReplaceTextParams,
EditorDecoration,
EditorMouseEvent,
EncodingMode,
EditorDecorationOptions,
MouseTargetType
} from '@theia/editor/lib/browser';
import { MonacoEditorModel } from './monaco-editor-model';
import { MonacoToProtocolConverter } from './monaco-to-protocol-converter';
import { ProtocolToMonacoConverter } from './protocol-to-monaco-converter';
import { TextEdit } from '@theia/core/shared/vscode-languageserver-protocol';
import { UTF8 } from '@theia/core/lib/common/encodings';
import * as monaco from '@theia/monaco-editor-core';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
import { ILanguageService } from '@theia/monaco-editor-core/esm/vs/editor/common/languages/language';
import { IInstantiationService, ServiceIdentifier } from '@theia/monaco-editor-core/esm/vs/platform/instantiation/common/instantiation';
import { ICodeEditor, IMouseTargetMargin } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorBrowser';
import { IStandaloneEditorConstructionOptions, StandaloneCodeEditor, StandaloneEditor } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneCodeEditor';
import { ServiceCollection } from '@theia/monaco-editor-core/esm/vs/platform/instantiation/common/serviceCollection';
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
import { ConfigurationChangedEvent, IEditorOptions, ShowLightbulbIconMode } from '@theia/monaco-editor-core/esm/vs/editor/common/config/editorOptions';
import { ICodeEditorService } from '@theia/monaco-editor-core/esm/vs/editor/browser/services/codeEditorService';
import { ICommandService } from '@theia/monaco-editor-core/esm/vs/platform/commands/common/commands';
import { IContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/common/contextkey';
import { IKeybindingService } from '@theia/monaco-editor-core/esm/vs/platform/keybinding/common/keybinding';
import { IThemeService } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/themeService';
import { INotificationService } from '@theia/monaco-editor-core/esm/vs/platform/notification/common/notification';
import { IAccessibilityService } from '@theia/monaco-editor-core/esm/vs/platform/accessibility/common/accessibility';
import { ILanguageConfigurationService } from '@theia/monaco-editor-core/esm/vs/editor/common/languages/languageConfigurationRegistry';
import { ILanguageFeaturesService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/languageFeatures';
import * as objects from '@theia/monaco-editor-core/esm/vs/base/common/objects';
import { Selection } from '@theia/editor/lib/browser/editor';
import { IHoverService, WorkbenchHoverDelegate } from '@theia/monaco-editor-core/esm/vs/platform/hover/browser/hover';
import { setHoverDelegateFactory } from '@theia/monaco-editor-core/esm/vs/base/browser/ui/hover/hoverDelegateFactory';
import { MonacoTextModelService } from './monaco-text-model-service';
export type ServicePair<T> = [ServiceIdentifier<T>, T];
export interface EditorServiceOverrides extends Iterable<ServicePair<unknown>> { }
@injectable()
export class MonacoEditorServices {
@inject(MonacoToProtocolConverter)
protected readonly m2p: MonacoToProtocolConverter;
@inject(ProtocolToMonacoConverter)
protected readonly p2m: ProtocolToMonacoConverter;
@inject(ContextKeyService)
protected readonly contextKeyService: ContextKeyService;
@inject(MonacoTextModelService)
protected readonly monacoModelService: MonacoTextModelService;
constructor(@unmanaged() services: MonacoEditorServices) {
Object.assign(this, services);
}
}
export class MonacoEditor extends MonacoEditorServices implements TextEditor {
static async create(uri: URI,
document: MonacoEditorModel,
node: HTMLElement,
services: MonacoEditorServices,
options?: MonacoEditor.IOptions,
override?: EditorServiceOverrides,
parentEditor?: MonacoEditor): Promise<MonacoEditor> {
const instance = new MonacoEditor(uri, document, node, services, options, override, parentEditor);
await instance.init();
return instance;
}
protected readonly toDispose = new DisposableCollection();
protected readonly autoSizing: boolean;
protected readonly minHeight: number;
protected readonly maxHeight: number;
protected editor: monaco.editor.IStandaloneCodeEditor;
protected readonly onCursorPositionChangedEmitter = new Emitter<Position>();
protected readonly onSelectionChangedEmitter = new Emitter<Selection>();
protected readonly onFocusChangedEmitter = new Emitter<boolean>();
protected readonly onDocumentContentChangedEmitter = new Emitter<TextDocumentChangeEvent>();
protected readonly onMouseDownEmitter = new Emitter<EditorMouseEvent>();
readonly onDidChangeReadOnly = this.document.onDidChangeReadOnly;
protected readonly onLanguageChangedEmitter = new Emitter<string>();
readonly onLanguageChanged = this.onLanguageChangedEmitter.event;
protected readonly onScrollChangedEmitter = new Emitter<void>();
readonly onEncodingChanged = this.document.onDidChangeEncoding;
protected readonly onResizeEmitter = new Emitter<Dimension | null>();
readonly onDidResize = this.onResizeEmitter.event;
protected readonly onShouldDisplayDirtyDiffChangedEmitter = new Emitter<boolean>;
readonly onShouldDisplayDirtyDiffChanged: Event<boolean> | undefined = this.onShouldDisplayDirtyDiffChangedEmitter.event;
readonly documents = new Set<MonacoEditorModel>();
// eslint-disable-next-line no-null/no-null
protected savedViewState: monaco.editor.IEditorViewState | null = null;
protected constructor(
readonly uri: URI,
readonly document: MonacoEditorModel,
readonly node: HTMLElement,
services: MonacoEditorServices,
options?: MonacoEditor.IOptions,
override?: EditorServiceOverrides,
readonly parentEditor?: MonacoEditor
) {
super(services);
this.toDispose.pushAll([
this.onCursorPositionChangedEmitter,
this.onSelectionChangedEmitter,
this.onFocusChangedEmitter,
this.onDocumentContentChangedEmitter,
this.onMouseDownEmitter,
this.onLanguageChangedEmitter,
this.onScrollChangedEmitter,
this.onShouldDisplayDirtyDiffChangedEmitter
]);
this.documents.add(document);
this.autoSizing = options && options.autoSizing !== undefined ? options.autoSizing : false;
this.minHeight = options && options.minHeight !== undefined ? options.minHeight : -1;
this.maxHeight = options && options.maxHeight !== undefined ? options.maxHeight : -1;
this.toDispose.push(this.create({
...MonacoEditor.createReadOnlyOptions(document.readOnly),
...options
}, override));
// Ensure that a valid InstantiationService is responsible for creating hover delegates when the InstantiationService for this widget is disposed.
// Cf. https://github.com/eclipse-theia/theia/issues/15102
this.toDispose.push(Disposable.create(() => setHoverDelegateFactory((placement, enableInstantHover) =>
StandaloneServices.get(IInstantiationService).createInstance(WorkbenchHoverDelegate, placement, enableInstantHover, {})
)));
this.addHandlers(this.editor);
this.editor.createContextKey('resource', document.uri);
}
protected async init(): Promise<void> {
this.toDispose.push(await this.monacoModelService.createModelReference(this.uri));
}
getEncoding(): string {
return this.document.getEncoding() || UTF8;
}
setEncoding(encoding: string, mode: EncodingMode): Promise<void> {
return this.document.setEncoding(encoding, mode);
}
protected create(options?: monaco.editor.IStandaloneEditorConstructionOptions | IStandaloneEditorConstructionOptions, override?: EditorServiceOverrides): Disposable {
const combinedOptions = {
...options,
lightbulb: { enabled: ShowLightbulbIconMode.On },
fixedOverflowWidgets: true,
scrollbar: {
useShadows: false,
verticalHasArrows: false,
horizontalHasArrows: false,
verticalScrollbarSize: 10,
horizontalScrollbarSize: 10,
...options?.scrollbar,
}
} as IStandaloneEditorConstructionOptions;
const instantiator = this.getInstantiatorWithOverrides(override);
/**
* @monaco-uplift. Should be guaranteed to work.
* Incomparable enums prevent TypeScript from believing that public IStandaloneCodeEditor is satisfied by private StandaloneCodeEditor
*/
return this.editor = (this.parentEditor ?
instantiator.createInstance(EmbeddedCodeEditor, this.node, combinedOptions, this.parentEditor.getControl() as unknown as ICodeEditor) :
instantiator.createInstance(StandaloneEditor, this.node, combinedOptions)) as unknown as monaco.editor.IStandaloneCodeEditor;
}
protected getInstantiatorWithOverrides(override?: EditorServiceOverrides): IInstantiationService {
const instantiator = StandaloneServices.get(IInstantiationService);
if (override) {
const overrideServices = new ServiceCollection(...override);
const child = instantiator.createChild(overrideServices);
this.toDispose.push(child);
return child;
}
return instantiator;
}
protected addHandlers(codeEditor: monaco.editor.IStandaloneCodeEditor): void {
this.toDispose.push(codeEditor.onDidChangeModelLanguage(e =>
this.fireLanguageChanged(e.newLanguage)
));
this.toDispose.push(codeEditor.onDidChangeConfiguration(() => this.refresh()));
this.toDispose.push(codeEditor.onDidChangeModel(() => this.refresh()));
this.toDispose.push(codeEditor.onDidChangeModelContent(e => {
this.refresh();
this.onDocumentContentChangedEmitter.fire({ document: this.document, contentChanges: e.changes.map(this.mapModelContentChange.bind(this)) });
}));
this.toDispose.push(codeEditor.onDidChangeCursorPosition(() =>
this.onCursorPositionChangedEmitter.fire(this.cursor)
));
this.toDispose.push(codeEditor.onDidChangeCursorSelection(event =>
this.onSelectionChangedEmitter.fire({
...this.m2p.asRange(event.selection),
direction: event.selection.getDirection() === monaco.SelectionDirection.LTR ? 'ltr' : 'rtl'
})
));
this.toDispose.push(codeEditor.onDidFocusEditorText(() =>
this.onFocusChangedEmitter.fire(this.isFocused())
));
this.toDispose.push(codeEditor.onDidBlurEditorText(() =>
this.onFocusChangedEmitter.fire(this.isFocused())
));
this.toDispose.push(codeEditor.onMouseDown(e => {
const { element, position, range } = e.target;
this.onMouseDownEmitter.fire({
target: {
type: e.target.type as unknown as MouseTargetType,
element: element || undefined,
mouseColumn: this.m2p.asPosition(undefined, e.target.mouseColumn).character,
range: range && this.m2p.asRange(range) || undefined,
position: position && this.m2p.asPosition(position.lineNumber, position.column) || undefined,
detail: (e.target as unknown as IMouseTargetMargin).detail || {},
},
event: e.event.browserEvent
});
}));
this.toDispose.push(codeEditor.onDidScrollChange(e => {
this.onScrollChangedEmitter.fire(undefined);
}));
this.toDispose.push(this.onDidChangeReadOnly(readOnly => {
codeEditor.updateOptions(MonacoEditor.createReadOnlyOptions(readOnly));
}));
}
handleVisibilityChanged(nowVisible: boolean): void {
if (nowVisible) {
this.baseEditor.setModel(this.baseModel);
this.baseEditor.restoreViewState(this.savedViewState);
this.baseEditor.focus();
} else {
this.savedViewState = this.baseEditor.saveViewState();
// eslint-disable-next-line no-null/no-null
this.baseEditor.setModel(null); // workaround for https://github.com/eclipse-theia/theia/issues/14880
}
}
/**
* This property allows working with the underlying editor instance
* through the base editor interface, `monaco.editor.IEditor`.
*
* This property is intended to be overriden in subclasses as needed,
* e.g. it returns the underlying diff editor in `MonacoDiffEditor`.
*/
protected get baseEditor(): monaco.editor.IEditor {
return this.editor;
}
/**
* This property allows working with the underlying editor model instance
* through the base editor model interface, `monaco.editor.IEditorModel`.
*
* This property is intended to be overriden in subclasses as needed,
* e.g. it returns the underlying diff editor model in `MonacoDiffEditor`.
*/
protected get baseModel(): monaco.editor.IEditorModel {
return this.document.textEditorModel;
}
getVisibleRanges(): Range[] {
return this.editor.getVisibleRanges().map(range => this.m2p.asRange(range));
}
protected mapModelContentChange(change: monaco.editor.IModelContentChange): TextDocumentContentChangeDelta {
return {
range: this.m2p.asRange(change.range),
rangeLength: change.rangeLength,
text: change.text
};
}
get onDispose(): Event<void> {
return this.toDispose.onDispose;
}
get onDocumentContentChanged(): Event<TextDocumentChangeEvent> {
return this.onDocumentContentChangedEmitter.event;
}
get isReadonly(): boolean | MarkdownString {
return this.document.readOnly;
}
get cursor(): Position {
const { lineNumber, column } = this.editor.getPosition()!;
return this.m2p.asPosition(lineNumber, column);
}
set cursor(cursor: Position) {
const position = this.p2m.asPosition(cursor);
this.editor.setPosition(position);
}
get onCursorPositionChanged(): Event<Position> {
return this.onCursorPositionChangedEmitter.event;
}
get selection(): Selection {
return this.m2p.asSelection(this.editor.getSelection()!);
}
set selection(selection: Selection) {
const range = this.p2m.asRange(selection);
this.editor.setSelection(range);
}
get onSelectionChanged(): Event<Selection> {
return this.onSelectionChangedEmitter.event;
}
get onScrollChanged(): Event<void> {
return this.onScrollChangedEmitter.event;
}
revealPosition(raw: Position, options: RevealPositionOptions = { vertical: 'center' }): void {
const position = this.p2m.asPosition(raw);
switch (options.vertical) {
case 'auto':
this.editor.revealPosition(position);
break;
case 'center':
this.editor.revealPositionInCenter(position);
break;
case 'centerIfOutsideViewport':
this.editor.revealPositionInCenterIfOutsideViewport(position);
break;
}
}
revealRange(raw: Range, options: RevealRangeOptions = { at: 'center' }): void {
const range = this.p2m.asRange(raw);
switch (options.at) {
case 'top':
this.editor.revealRangeAtTop(range!);
break;
case 'center':
this.editor.revealRangeInCenter(range!);
break;
case 'centerIfOutsideViewport':
this.editor.revealRangeInCenterIfOutsideViewport(range!);
break;
case 'auto':
this.editor.revealRange(range!);
break;
}
}
focus(): void {
/**
* `this.editor.focus` forcefully changes the focus editor state,
* regardless whether the textarea actually received the focus.
* It could lead to issues like https://github.com/eclipse-theia/theia/issues/7902
* Instead we focus the underlying textarea.
*/
const node = this.editor.getDomNode();
if (node) {
const textarea = node.querySelector('textarea') as HTMLElement;
textarea.focus();
}
}
blur(): void {
const node = this.editor.getDomNode();
if (node) {
const textarea = node.querySelector('textarea') as HTMLElement;
textarea.blur();
}
}
isFocused({ strict }: { strict: boolean } = { strict: false }): boolean {
if (!this.editor.hasTextFocus()) {
return false;
}
if (strict) {
return !this.isSuggestWidgetVisible() && !this.isFindWidgetVisible() && !this.isRenameInputVisible();
}
return true;
}
get onFocusChanged(): Event<boolean> {
return this.onFocusChangedEmitter.event;
}
get onMouseDown(): Event<EditorMouseEvent> {
return this.onMouseDownEmitter.event;
}
/**
* `true` if the suggest widget is visible in the editor. Otherwise, `false`.
*/
isSuggestWidgetVisible(): boolean {
return this.contextKeyService.match('suggestWidgetVisible', this.editor.getDomNode() || this.node);
}
/**
* `true` if the find (and replace) widget is visible in the editor. Otherwise, `false`.
*/
isFindWidgetVisible(): boolean {
return this.contextKeyService.match('findWidgetVisible', this.editor.getDomNode() || this.node);
}
/**
* `true` if the name rename refactoring input HTML element is visible. Otherwise, `false`.
*/
isRenameInputVisible(): boolean {
return this.contextKeyService.match('renameInputVisible', this.editor.getDomNode() || this.node);
}
dispose(): void {
this.toDispose.dispose();
}
isDisposed(): boolean {
return this.toDispose.disposed;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
trigger(source: string, handlerId: string, payload: any): void {
this.editor.trigger(source, handlerId, payload);
}
getControl(): monaco.editor.IStandaloneCodeEditor {
return this.editor;
}
refresh(): void {
this.autoresize();
}
resizeToFit(): void {
this.autoresize();
// eslint-disable-next-line no-null/no-null
this.onResizeEmitter.fire(null);
}
setSize(dimension: Dimension): void {
this.resize(dimension);
this.onResizeEmitter.fire(dimension);
}
protected autoresize(): void {
if (this.autoSizing) {
// eslint-disable-next-line no-null/no-null
this.resize(null);
}
}
protected resize(dimension: Dimension | null): void {
if (this.node) {
const layoutSize = this.computeLayoutSize(this.node, dimension);
this.editor.layout(layoutSize);
}
}
protected computeLayoutSize(hostNode: HTMLElement, dimension: monaco.editor.IDimension | null): monaco.editor.IDimension {
if (dimension && dimension.width >= 0 && dimension.height >= 0) {
return dimension;
}
const boxSizing = ElementExt.boxSizing(hostNode);
const width = (!dimension || dimension.width < 0) ?
this.getWidth(hostNode, boxSizing) :
dimension.width;
const height = (!dimension || dimension.height < 0) ?
this.getHeight(hostNode, boxSizing) :
dimension.height;
return { width, height };
}
protected getWidth(hostNode: HTMLElement, boxSizing: ElementExt.IBoxSizing): number {
return hostNode.offsetWidth - boxSizing.horizontalSum;
}
protected getHeight(hostNode: HTMLElement, boxSizing: ElementExt.IBoxSizing): number {
if (!this.autoSizing) {
return hostNode.offsetHeight - boxSizing.verticalSum;
}
const lineHeight = this.editor.getOption(monaco.editor.EditorOption.lineHeight);
const lineCount = this.editor.getModel()!.getLineCount();
const contentHeight = lineHeight * lineCount;
const horizontalScrollbarHeight = this.editor.getLayoutInfo().horizontalScrollbarHeight;
const editorHeight = contentHeight + horizontalScrollbarHeight;
if (this.minHeight >= 0) {
const minHeight = lineHeight * this.minHeight + horizontalScrollbarHeight;
if (editorHeight < minHeight) {
return minHeight;
}
}
if (this.maxHeight >= 0) {
const maxHeight = lineHeight * this.maxHeight + horizontalScrollbarHeight;
return Math.min(maxHeight, editorHeight);
}
return editorHeight;
}
isActionSupported(id: string): boolean {
const action = this.editor.getAction(id);
return !!action && action.isSupported();
}
async runAction(id: string): Promise<void> {
const action = this.editor.getAction(id);
if (action && action.isSupported()) {
await action.run();
}
}
deltaDecorations(params: DeltaDecorationParams): string[] {
const oldDecorations = params.oldDecorations;
const newDecorations = this.toDeltaDecorations(params);
return this.editor.deltaDecorations(oldDecorations, newDecorations);
}
protected toDeltaDecorations(params: DeltaDecorationParams): monaco.editor.IModelDeltaDecoration[] {
return params.newDecorations.map(({ options: theiaOptions, range }) => {
const options: monaco.editor.IModelDecorationOptions = {
...theiaOptions,
hoverMessage: this.fromStringToMarkdownString(theiaOptions.hoverMessage),
glyphMarginHoverMessage: this.fromStringToMarkdownString(theiaOptions.glyphMarginHoverMessage)
};
return {
options,
range: this.p2m.asRange(range),
};
});
}
protected fromStringToMarkdownString(hoverMessage?: string | monaco.IMarkdownString | monaco.IMarkdownString[]): monaco.IMarkdownString | monaco.IMarkdownString[] | undefined {
if (typeof hoverMessage === 'string') {
return { value: hoverMessage };
}
return hoverMessage;
}
protected fromMarkdownToString(maybeMarkdown?: null | string | monaco.IMarkdownString | monaco.IMarkdownString[]): string | undefined {
if (!maybeMarkdown) {
return undefined;
}
if (typeof maybeMarkdown === 'string') {
return maybeMarkdown;
}
if (Array.isArray(maybeMarkdown)) {
return maybeMarkdown.map(({ value }) => value).join('\n');
}
return maybeMarkdown.value;
}
getLinesDecorations(startLineNumber: number, endLineNumber: number): (EditorDecoration & Readonly<{ id: string }>)[] {
const toPosition = (line: number): monaco.Position => this.p2m.asPosition({ line, character: 0 });
const start = toPosition(startLineNumber).lineNumber;
const end = toPosition(endLineNumber).lineNumber;
return this.editor.getModel()?.getLinesDecorations(start, end)
.map(this.toEditorDecoration.bind(this)) || [];
}
protected toEditorDecoration(decoration: monaco.editor.IModelDecoration): EditorDecoration & Readonly<{ id: string }> {
const range = this.m2p.asRange(decoration.range);
const { id, options: monacoOptions } = decoration;
const options: MaybeNull<EditorDecorationOptions> = {
...monacoOptions,
hoverMessage: this.fromMarkdownToString(monacoOptions.hoverMessage),
glyphMarginHoverMessage: this.fromMarkdownToString(monacoOptions.hoverMessage),
};
return {
options: nullToUndefined(options),
range,
id
};
}
getVisibleColumn(position: Position): number {
return this.editor.getVisibleColumnFromPosition(this.p2m.asPosition(position));
}
async replaceText(params: ReplaceTextParams): Promise<boolean> {
const edits: monaco.editor.IIdentifiedSingleEditOperation[] = params.replaceOperations.map(param => {
const range = monaco.Range.fromPositions(this.p2m.asPosition(param.range.start), this.p2m.asPosition(param.range.end));
return {
forceMoveMarkers: true,
identifier: {
major: range.startLineNumber,
minor: range.startColumn
},
range,
text: param.text
};
});
// If the editor doesn't have a model set (e.g., widget not visible),
// apply edits directly to the document's underlying model.
// This ensures text replacements work even when the editor widget is not attached to the shell.
if (!this.editor.getModel()) {
this.document.textEditorModel.applyEdits(edits);
return true;
}
return this.editor.executeEdits(params.source, edits);
}
executeEdits(edits: TextEdit[]): boolean {
const monacoEdits = this.p2m.asTextEdits(edits) as monaco.editor.IIdentifiedSingleEditOperation[];
// If the editor doesn't have a model set (e.g., widget not visible),
// apply edits directly to the document's underlying model.
if (!this.editor.getModel()) {
this.document.textEditorModel.applyEdits(monacoEdits);
return true;
}
return this.editor.executeEdits('MonacoEditor', monacoEdits);
}
storeViewState(): object {
if (this.baseEditor.getModel()) {
this.savedViewState = this.baseEditor.saveViewState();
}
return this.savedViewState!;
}
restoreViewState(state: monaco.editor.IEditorViewState): void {
if (this.baseEditor.getModel()) {
this.baseEditor.restoreViewState(state);
}
this.savedViewState = state;
}
/* `true` because it is derived from an URI during the instantiation */
protected _languageAutoDetected = true;
get languageAutoDetected(): boolean {
return this._languageAutoDetected;
}
async detectLanguage(): Promise<void> {
const languageService = StandaloneServices.get(ILanguageService);
const firstLine = this.document.textEditorModel.getLineContent(1);
const model = this.getControl().getModel();
const language = languageService.createByFilepathOrFirstLine(model && model.uri, firstLine);
this.setLanguage(language.languageId);
this._languageAutoDetected = true;
}
setLanguage(languageId: string): void {
for (const document of this.documents) {
monaco.editor.setModelLanguage(document.textEditorModel, languageId);
}
}
protected fireLanguageChanged(languageId: string): void {
this._languageAutoDetected = false;
this.onLanguageChangedEmitter.fire(languageId);
}
getResourceUri(): URI {
return this.uri;
}
createMoveToUri(resourceUri: URI): URI {
return this.uri.withPath(resourceUri.path);
}
private _shouldDisplayDirtyDiff = true;
shouldDisplayDirtyDiff(): boolean {
return this._shouldDisplayDirtyDiff;
}
setShouldDisplayDirtyDiff(value: boolean): void {
if (value !== this._shouldDisplayDirtyDiff) {
this._shouldDisplayDirtyDiff = value;
this.onShouldDisplayDirtyDiffChangedEmitter.fire(value);
}
}
}
export namespace MonacoEditor {
export interface ICommonOptions {
/**
* Whether an editor should be auto resized on a content change.
*
* #### Fixme
* remove when https://github.com/Microsoft/monaco-editor/issues/103 is resolved
*/
autoSizing?: boolean;
/**
* A minimal height of an editor in lines.
*
* #### Fixme
* remove when https://github.com/Microsoft/monaco-editor/issues/103 is resolved
*/
minHeight?: number;
/**
* A maximal height of an editor in lines.
*
* #### Fixme
* remove when https://github.com/Microsoft/monaco-editor/issues/103 is resolved
*/
maxHeight?: number;
}
export interface IOptions extends ICommonOptions, monaco.editor.IStandaloneEditorConstructionOptions { }
export function getAll(manager: EditorManager): MonacoEditor[] {
return manager.all.map(e => get(e)).filter(e => !!e) as MonacoEditor[];
}
export function getCurrent(manager: EditorManager): MonacoEditor | undefined {
return get(manager.currentEditor);
}
export function getActive(manager: EditorManager): MonacoEditor | undefined {
return get(manager.activeEditor);
}
export function get(editorWidget: EditorWidget | undefined): MonacoEditor | undefined {
if (editorWidget && editorWidget.editor instanceof MonacoEditor) {
return editorWidget.editor;
}
return undefined;
}
export function findByDocument(manager: EditorManager, document: MonacoEditorModel): MonacoEditor[] {
return getAll(manager).filter(candidate => candidate.documents.has(document));
}
export function getWidgetFor(manager: EditorManager, control: monaco.editor.ICodeEditor | ICodeEditor | undefined | null): EditorWidget | undefined {
if (!control) {
return undefined;
}
return manager.all.find(widget => {
const candidate = get(widget);
return candidate && candidate.getControl() === control;
});
}
export function createReadOnlyOptions(readOnly?: boolean | MarkdownString): monaco.editor.IEditorOptions {
if (typeof readOnly === 'boolean') {
return { readOnly, readOnlyMessage: undefined };
}
if (readOnly) {
return { readOnly: true, readOnlyMessage: readOnly };
}
return {};
}
}
// adapted from https://github.com/microsoft/vscode/blob/0bd70d48ad8b3e2fb1922aa54f87c786ff2b4bd8/src/vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget.ts
// This class reproduces the logic in EmbeddedCodeEditorWidget but extends StandaloneCodeEditor rather than CodeEditorWidget.
class EmbeddedCodeEditor extends StandaloneCodeEditor {
private readonly _parentEditor: ICodeEditor;
private readonly _overwriteOptions: IEditorOptions;
constructor(
domElement: HTMLElement,
options: Readonly<IStandaloneEditorConstructionOptions>,
parentEditor: ICodeEditor,
@IInstantiationService instantiationService: IInstantiationService,
@ICodeEditorService codeEditorService: ICodeEditorService,
@ICommandService commandService: ICommandService,
@IContextKeyService contextKeyService: IContextKeyService,
@IKeybindingService keybindingService: IKeybindingService,
@IThemeService themeService: IThemeService,
@INotificationService notificationService: INotificationService,
@IAccessibilityService accessibilityService: IAccessibilityService,
@ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService,
@ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService,
@IHoverService hoverService: IHoverService
) {
super(domElement,
{ ...parentEditor.getRawOptions(), overflowWidgetsDomNode: parentEditor.getOverflowWidgetsDomNode() },
instantiationService,
codeEditorService,
commandService,
contextKeyService,
hoverService,
keybindingService,
themeService,
notificationService,
accessibilityService,
languageConfigurationService,
languageFeaturesService);
this._parentEditor = parentEditor;
this._overwriteOptions = options;
// Overwrite parent's options
super.updateOptions(this._overwriteOptions);
this._register(parentEditor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => this._onParentConfigurationChanged(e)));
}
getParentEditor(): ICodeEditor {
return this._parentEditor;
}
private _onParentConfigurationChanged(e: ConfigurationChangedEvent): void {
super.updateOptions(this._parentEditor.getRawOptions());
super.updateOptions(this._overwriteOptions);
}
override updateOptions(newOptions: IEditorOptions): void {
objects.mixin(this._overwriteOptions, newOptions, true);
super.updateOptions(this._overwriteOptions);
}
}

View File

@@ -0,0 +1,117 @@
// *****************************************************************************
// Copyright (C) 2020 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject } from '@theia/core/shared/inversify';
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { EditorManager } from '@theia/editor/lib/browser';
import { MonacoQuickInputService } from './monaco-quick-input-service';
import * as monaco from '@theia/monaco-editor-core';
import { FormattingConflicts, FormattingMode } from '@theia/monaco-editor-core/esm/vs/editor/contrib/format/browser/format';
import { DocumentFormattingEditProvider, DocumentRangeFormattingEditProvider } from '@theia/monaco-editor-core/esm/vs/editor/common/languages';
import { ITextModel } from '@theia/monaco-editor-core/esm/vs/editor/common/model';
import { nls } from '@theia/core/lib/common/nls';
import { PreferenceService, PreferenceLanguageOverrideService } from '@theia/core';
type FormattingEditProvider = DocumentFormattingEditProvider | DocumentRangeFormattingEditProvider;
const PREFERENCE_NAME = 'editor.defaultFormatter';
@injectable()
export class MonacoFormattingConflictsContribution implements FrontendApplicationContribution {
@inject(MonacoQuickInputService)
protected readonly monacoQuickInputService: MonacoQuickInputService;
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
@inject(PreferenceLanguageOverrideService)
protected readonly preferenceSchema: PreferenceLanguageOverrideService;
@inject(EditorManager)
protected readonly editorManager: EditorManager;
async initialize(): Promise<void> {
FormattingConflicts.setFormatterSelector(<T extends FormattingEditProvider>(
formatters: T[], document: ITextModel, mode: FormattingMode) =>
this.selectFormatter(formatters, document, mode));
}
protected async setDefaultFormatter(language: string, formatter: string): Promise<void> {
const name = this.preferenceSchema.overridePreferenceName({
preferenceName: PREFERENCE_NAME,
overrideIdentifier: language
});
await this.preferenceService.set(name, formatter);
}
private getDefaultFormatter(language: string, resourceURI: string): string | undefined {
const name = this.preferenceSchema.overridePreferenceName({
preferenceName: PREFERENCE_NAME,
overrideIdentifier: language
});
return this.preferenceService.get<string>(name, undefined, resourceURI);
}
private async selectFormatter<T extends FormattingEditProvider>(
formatters: T[], document: monaco.editor.ITextModel | ITextModel, mode: FormattingMode): Promise<T | undefined> {
if (formatters.length === 0) {
return undefined;
}
if (formatters.length === 1) {
return formatters[0];
}
const currentEditor = this.editorManager.currentEditor;
if (!currentEditor) {
return undefined;
}
const languageId = currentEditor.editor.document.languageId;
const defaultFormatterId = this.getDefaultFormatter(languageId, document.uri.toString());
if (defaultFormatterId) {
const formatter = formatters.find(f => f.extensionId && f.extensionId.value === defaultFormatterId);
if (formatter) {
return formatter;
}
}
return new Promise<T | undefined>(async (resolve, reject) => {
const items = formatters
.filter(formatter => formatter.displayName)
.map(formatter => ({
label: formatter.displayName!,
detail: formatter.extensionId ? formatter.extensionId.value : undefined,
value: formatter,
}))
.sort((a, b) => a.label!.localeCompare(b.label!));
const selectedFormatter = await this.monacoQuickInputService.showQuickPick(items, { placeholder: nls.localizeByDefault('Format Document With...') });
if (selectedFormatter) {
this.setDefaultFormatter(languageId, selectedFormatter.detail ? selectedFormatter.detail : '');
resolve(selectedFormatter.value);
} else {
resolve(undefined);
}
});
}
}

View File

@@ -0,0 +1,203 @@
// *****************************************************************************
// Copyright (C) 2018 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { ColorTheme, CssStyleCollector, FrontendApplicationContribution, QuickAccessRegistry, StylingParticipant } from '@theia/core/lib/browser';
import { MonacoSnippetSuggestProvider } from './monaco-snippet-suggest-provider';
import * as monaco from '@theia/monaco-editor-core';
import { setSnippetSuggestSupport } from '@theia/monaco-editor-core/esm/vs/editor/contrib/suggest/browser/suggest';
import { CompletionItemProvider } from '@theia/monaco-editor-core/esm/vs/editor/common/languages';
import { MonacoTextModelService } from './monaco-text-model-service';
import { MonacoThemingService } from './monaco-theming-service';
import { isHighContrast } from '@theia/core/lib/common/theme';
import { editorOptionsRegistry, IEditorOption } from '@theia/monaco-editor-core/esm/vs/editor/common/config/editorOptions';
import { MAX_SAFE_INTEGER, PreferenceSchemaService } from '@theia/core';
import { editorGeneratedPreferenceProperties } from '@theia/editor/lib/common/editor-generated-preference-schema';
import { WorkspaceFileService } from '@theia/workspace/lib/common/workspace-file-service';
import { SecondaryWindowHandler } from '@theia/core/lib/browser/secondary-window-handler';
import { EditorWidget } from '@theia/editor/lib/browser';
import { MonacoEditor } from './monaco-editor';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
import { StandaloneThemeService } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneThemeService';
import { IStandaloneThemeService } from '@theia/monaco-editor-core/esm/vs/editor/standalone/common/standaloneTheme';
import { SecondaryWindowService } from '@theia/core/lib/browser/window/secondary-window-service';
import { registerWindow } from '@theia/monaco-editor-core/esm/vs/base/browser/dom';
type CodeWindow = Window & typeof globalThis & {
vscodeWindowId: number;
};
@injectable()
export class MonacoFrontendApplicationContribution implements FrontendApplicationContribution, StylingParticipant {
protected readonly windowsById = new Map<number, monaco.IDisposable>();
protected nextWindowId = 2; // the main window has the id "1"
@inject(MonacoTextModelService)
protected readonly textModelService: MonacoTextModelService;
@inject(MonacoSnippetSuggestProvider)
protected readonly snippetSuggestProvider: MonacoSnippetSuggestProvider;
@inject(PreferenceSchemaService)
protected readonly preferenceSchema: PreferenceSchemaService;
@inject(QuickAccessRegistry)
protected readonly quickAccessRegistry: QuickAccessRegistry;
@inject(MonacoThemingService) protected readonly monacoThemingService: MonacoThemingService;
@inject(WorkspaceFileService) protected readonly workspaceFileService: WorkspaceFileService;
@inject(SecondaryWindowHandler)
protected readonly secondaryWindowHandler: SecondaryWindowHandler;
@inject(SecondaryWindowService)
protected readonly secondaryWindowService: SecondaryWindowService;
@postConstruct()
protected init(): void {
this.addAdditionalPreferenceValidations();
// Monaco registers certain quick access providers (e.g. QuickCommandAccess) at import time, but we want to use our own.
this.quickAccessRegistry.clear();
/**
* @monaco-uplift.Should be guaranteed to work.
* Incomparable enums prevent TypeScript from believing that public ITextModel satisfied private ITextModel
*/
setSnippetSuggestSupport(this.snippetSuggestProvider as unknown as CompletionItemProvider);
for (const language of monaco.languages.getLanguages()) {
this.preferenceSchema.registerOverrideIdentifier(language.id);
}
const registerLanguage = monaco.languages.register.bind(monaco.languages);
monaco.languages.register = language => {
// first register override identifier, because monaco will immediately update already opened documents and then initialize with bad preferences.
this.preferenceSchema.registerOverrideIdentifier(language.id);
registerLanguage(language);
};
this.monacoThemingService.initialize();
}
initialize(): void {
const workspaceExtensions = this.workspaceFileService.getWorkspaceFileExtensions();
monaco.languages.register({
id: 'jsonc',
'aliases': [
'JSON with Comments'
],
'extensions': workspaceExtensions.map(ext => `.${ext}`)
});
}
onStart(): void {
this.secondaryWindowHandler.onDidAddWidget(([widget, window]) => {
if (widget instanceof EditorWidget && widget.editor instanceof MonacoEditor) {
const themeService = StandaloneServices.get(IStandaloneThemeService) as StandaloneThemeService;
themeService.registerEditorContainer(widget.node);
}
});
this.secondaryWindowService.onWindowOpened(window => {
const codeWindow: CodeWindow = window as CodeWindow;
codeWindow.vscodeWindowId = this.nextWindowId++;
this.windowsById.set(codeWindow.vscodeWindowId, registerWindow(codeWindow));
});
this.secondaryWindowService.onWindowClosed(window => {
const codeWindow: CodeWindow = window as CodeWindow;
this.windowsById.get(codeWindow.vscodeWindowId)?.dispose();
});
}
registerThemeStyle(theme: ColorTheme, collector: CssStyleCollector): void {
if (isHighContrast(theme.type)) {
const focusBorder = theme.getColor('focusBorder');
const contrastBorder = theme.getColor('contrastBorder');
if (focusBorder) {
// Quick input
collector.addRule(`
.quick-input-list .monaco-list-row {
outline-offset: -1px;
}
.quick-input-list .monaco-list-row.focused {
outline: 1px dotted ${focusBorder};
}
.quick-input-list .monaco-list-row:hover {
outline: 1px dashed ${focusBorder};
}
`);
// Input box always displays an outline, even when unfocused
collector.addRule(`
.monaco-editor .find-widget .monaco-inputbox {
outline: var(--theia-border-width) solid;
outline-offset: calc(-1 * var(--theia-border-width));
outline-color: var(--theia-focusBorder);
}
`);
}
if (contrastBorder) {
collector.addRule(`
.quick-input-widget {
outline: 1px solid ${contrastBorder};
outline-offset: -1px;
}
`);
}
} else {
collector.addRule(`
.quick-input-widget {
box-shadow: rgb(0 0 0 / 36%) 0px 0px 8px 2px;
}
`);
}
}
/**
* For reasons that are unclear, while most preferences that apply in editors are validated, a few are not.
* There is a utility in `examples/api-samples/src/browser/monaco-editor-preferences/monaco-editor-preference-extractor.ts` to help determine which are not.
* Check `src/vs/editor/common/config/editorOptions.ts` for constructor arguments and to make sure that the preference names used to extract constructors are still accurate.
*/
protected addAdditionalPreferenceValidations(): void {
let editorIntConstructor: undefined | (new (...args: unknown[]) => IEditorOption<number, number>);
let editorBoolConstructor: undefined | (new (...args: unknown[]) => IEditorOption<number, boolean>);
let editorStringEnumConstructor: undefined | (new (...args: unknown[]) => IEditorOption<number, string>);
for (const validator of editorOptionsRegistry) {
/* eslint-disable @typescript-eslint/no-explicit-any,max-len */
if (editorIntConstructor && editorBoolConstructor && editorStringEnumConstructor) { break; }
if (validator.name === 'acceptSuggestionOnCommitCharacter') {
editorBoolConstructor = validator.constructor as any;
} else if (validator.name === 'acceptSuggestionOnEnter') {
editorStringEnumConstructor = validator.constructor as any;
} else if (validator.name === 'accessibilityPageSize') {
editorIntConstructor = validator.constructor as any;
}
/* eslint-enable @typescript-eslint/no-explicit-any */
}
if (editorIntConstructor && editorBoolConstructor && editorStringEnumConstructor) {
let id = 200; // Needs to be bigger than the biggest index in the EditorOption enum.
editorOptionsRegistry.push(
new editorIntConstructor(id++, 'tabSize', 4, 1, MAX_SAFE_INTEGER, editorGeneratedPreferenceProperties['editor.tabSize']),
new editorBoolConstructor(id++, 'insertSpaces', true, editorGeneratedPreferenceProperties['editor.insertSpaces']),
new editorBoolConstructor(id++, 'detectIndentation', true, editorGeneratedPreferenceProperties['editor.detectIndentation']),
new editorBoolConstructor(id++, 'trimAutoWhitespace', true, editorGeneratedPreferenceProperties['editor.trimAutoWhitespace']),
new editorBoolConstructor(id++, 'largeFileOptimizations', true, editorGeneratedPreferenceProperties['editor.largeFileOptimizations']),
new editorStringEnumConstructor(id++, 'wordBasedSuggestions', 'matchingDocuments', editorGeneratedPreferenceProperties['editor.wordBasedSuggestions'].enum, editorGeneratedPreferenceProperties['editor.wordBasedSuggestions']),
new editorBoolConstructor(id++, 'stablePeek', false, editorGeneratedPreferenceProperties['editor.stablePeek']),
new editorIntConstructor(id++, 'maxTokenizationLineLength', 20000, 1, MAX_SAFE_INTEGER, editorGeneratedPreferenceProperties['editor.maxTokenizationLineLength']),
);
}
}
}

View File

@@ -0,0 +1,352 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import '../../src/browser/style/index.css';
import { ContainerModule, interfaces } from '@theia/core/shared/inversify';
import {
MenuContribution, CommandContribution, quickInputServicePath, createPreferenceProxy,
OVERRIDE_PROPERTY_PATTERN, PreferenceChange, PreferenceScope, PreferenceService,
PreferenceSchemaService
} from '@theia/core/lib/common';
import {
FrontendApplicationContribution, KeybindingContribution, QuickInputService, StylingParticipant, WebSocketConnectionProvider, UndoRedoHandler, WidgetStatusBarContribution
} from '@theia/core/lib/browser';
import { TextEditorProvider, DiffNavigatorProvider, TextEditor } from '@theia/editor/lib/browser';
import { MonacoEditorProvider, MonacoEditorFactory, SaveParticipant } from './monaco-editor-provider';
import { MonacoEditorMenuContribution } from './monaco-menu';
import { MonacoEditorCommandHandlers } from './monaco-command';
import { MonacoKeybindingContribution } from './monaco-keybinding';
import { MonacoLanguages } from './monaco-languages';
import { MonacoWorkspace } from './monaco-workspace';
import { ActiveMonacoEditorContribution, MonacoEditorService, MonacoEditorServiceFactory, VSCodeContextKeyService, VSCodeThemeService } from './monaco-editor-service';
import { MonacoTextModelService, MonacoEditorModelFactory, MonacoEditorModelFilter } from './monaco-text-model-service';
import { MonacoContextMenuService } from './monaco-context-menu';
import { MonacoOutlineContribution } from './monaco-outline-contribution';
import { MonacoStatusBarContribution } from './monaco-status-bar-contribution';
import { MonacoCommandService } from './monaco-command-service';
import { MonacoCommandRegistry } from './monaco-command-registry';
import { MonacoDiffNavigatorFactory } from './monaco-diff-navigator-factory';
import { MonacoFrontendApplicationContribution } from './monaco-frontend-application-contribution';
import MonacoTextmateModuleBinder from './textmate/monaco-textmate-frontend-bindings';
import { MonacoBulkEditService } from './monaco-bulk-edit-service';
import { MonacoOutlineDecorator } from './monaco-outline-decorator';
import { OutlineTreeDecorator } from '@theia/outline-view/lib/browser/outline-decorator-service';
import { MonacoSnippetSuggestProvider } from './monaco-snippet-suggest-provider';
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
import { MonacoContextKeyService } from './monaco-context-key-service';
import { MonacoMimeService } from './monaco-mime-service';
import { MimeService } from '@theia/core/lib/browser/mime-service';
import { MonacoEditorServices } from './monaco-editor';
import { MonacoColorRegistry } from './monaco-color-registry';
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
import { MonacoIconRegistry } from './monaco-icon-registry';
import { IconRegistry } from '@theia/core/lib/browser/icon-registry';
import { MonacoThemingService } from './monaco-theming-service';
import { bindContributionProvider } from '@theia/core';
import { WorkspaceSymbolCommand } from './workspace-symbol-command';
import { LanguageService } from '@theia/core/lib/browser/language-service';
import { MonacoToProtocolConverter } from './monaco-to-protocol-converter';
import { ProtocolToMonacoConverter } from './protocol-to-monaco-converter';
import { MonacoFormattingConflictsContribution } from './monaco-formatting-conflicts';
import { MonacoQuickInputImplementation, MonacoQuickInputService } from './monaco-quick-input-service';
import { GotoLineQuickAccessContribution } from './monaco-gotoline-quick-access';
import { GotoSymbolQuickAccessContribution } from './monaco-gotosymbol-quick-access';
import { QuickAccessContribution, QuickAccessRegistry } from '@theia/core/lib/browser/quick-input/quick-access';
import { MonacoQuickAccessRegistry } from './monaco-quick-access-registry';
import { ConfigurationTarget, IConfigurationChangeEvent, IConfigurationService } from '@theia/monaco-editor-core/esm/vs/platform/configuration/common/configuration.js';
import { StandaloneConfigurationService, StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
import { Configuration } from '@theia/monaco-editor-core/esm/vs/platform/configuration/common/configurationModels.js';
import { MarkdownRenderer } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer';
import { MonacoMarkdownRenderer } from './markdown-renderer/monaco-markdown-renderer';
import { ThemeService } from '@theia/core/lib/browser/theming';
import { ThemeServiceWithDB } from './monaco-indexed-db';
import { IContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/common/contextkey.js';
import { IThemeService } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/themeService.js';
import { ActiveMonacoUndoRedoHandler, FocusedMonacoUndoRedoHandler } from './monaco-undo-redo-handler';
import { ILogService } from '@theia/monaco-editor-core/esm/vs/platform/log/common/log';
import { DefaultContentHoverWidgetPatcher } from './default-content-hover-widget-patcher';
import { MonacoWorkspaceContextService } from './monaco-workspace-context-service';
import { MonacoCodeActionSaveParticipant } from './monaco-code-action-save-participant';
import { MonacoCodeActionService, MonacoCodeActionServiceImpl } from './monaco-code-action-service';
import { MonacoDiffComputer } from './monaco-diff-computer';
import { DiffComputer } from '@theia/core/lib/common/diff';
import { MonacoEditorContentMenuContribution } from './monaco-editor-content-menu';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(MonacoThemingService).toSelf().inSingletonScope();
bind(MonacoContextKeyService).toSelf().inSingletonScope();
rebind(ContextKeyService).toService(MonacoContextKeyService);
bind(MonacoSnippetSuggestProvider).toSelf().inSingletonScope();
bind(MonacoFrontendApplicationContribution).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(MonacoFrontendApplicationContribution);
bind(StylingParticipant).toService(MonacoFrontendApplicationContribution);
bind(MonacoCodeActionServiceImpl).toSelf().inSingletonScope();
bind(MonacoCodeActionService).toService(MonacoCodeActionServiceImpl);
bind(MonacoCodeActionSaveParticipant).toSelf().inSingletonScope();
bind(SaveParticipant).toService(MonacoCodeActionSaveParticipant);
bind(MonacoToProtocolConverter).toSelf().inSingletonScope();
bind(ProtocolToMonacoConverter).toSelf().inSingletonScope();
bind(MonacoLanguages).toSelf().inSingletonScope();
rebind(LanguageService).toService(MonacoLanguages);
bind(WorkspaceSymbolCommand).toSelf().inSingletonScope();
for (const identifier of [CommandContribution, KeybindingContribution, MenuContribution, QuickAccessContribution]) {
bind(identifier).toService(WorkspaceSymbolCommand);
}
bind(MonacoWorkspace).toSelf().inSingletonScope();
bind(MonacoWorkspaceContextService).toSelf().inSingletonScope();
bind(MonacoConfigurationService).toDynamicValue(({ container }) => createMonacoConfigurationService(container)).inSingletonScope();
bind(MonacoBulkEditService).toSelf().inSingletonScope();
bindContributionProvider(bind, ActiveMonacoEditorContribution);
bind(MonacoEditorServiceFactory).toFactory((context: interfaces.Context) => (contextKeyService: IContextKeyService, themeService: IThemeService) => {
const child = context.container.createChild();
child.bind(VSCodeContextKeyService).toConstantValue(contextKeyService);
child.bind(VSCodeThemeService).toConstantValue(themeService);
child.bind(MonacoEditorService).toSelf().inSingletonScope();
return child.get(MonacoEditorService);
});
bind(MonacoTextModelService).toSelf().inSingletonScope();
bind(MonacoContextMenuService).toSelf().inSingletonScope();
bind(MonacoEditorServices).toSelf().inSingletonScope();
bind(MonacoEditorProvider).toSelf().inSingletonScope();
bindContributionProvider(bind, MonacoEditorFactory);
bindContributionProvider(bind, MonacoEditorModelFactory);
bindContributionProvider(bind, MonacoEditorModelFilter);
bindContributionProvider(bind, SaveParticipant);
bind(MonacoCommandService).toSelf().inTransientScope();
bind(TextEditorProvider).toProvider(context =>
uri => context.container.get(MonacoEditorProvider).get(uri)
);
bind(MonacoDiffNavigatorFactory).toSelf().inSingletonScope();
bind(DiffNavigatorProvider).toFactory(context =>
(editor: TextEditor) => context.container.get(MonacoEditorProvider).getDiffNavigator(editor)
);
bind(MonacoOutlineContribution).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(MonacoOutlineContribution);
rebind(MarkdownRenderer).to(MonacoMarkdownRenderer).inSingletonScope();
bind(MonacoFormattingConflictsContribution).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(MonacoFormattingConflictsContribution);
bind(MonacoStatusBarContribution).toSelf().inSingletonScope();
bind(WidgetStatusBarContribution).toService(MonacoStatusBarContribution);
bind(MonacoCommandRegistry).toSelf().inSingletonScope();
bind(MonacoEditorCommandHandlers).toSelf().inSingletonScope();
bind(CommandContribution).toService(MonacoEditorCommandHandlers);
bind(MonacoEditorMenuContribution).toSelf().inSingletonScope();
bind(MenuContribution).toService(MonacoEditorMenuContribution);
bind(MonacoKeybindingContribution).toSelf().inSingletonScope();
bind(KeybindingContribution).toService(MonacoKeybindingContribution);
bind(MonacoQuickInputImplementation).toSelf().inSingletonScope();
bind(MonacoQuickInputService).toSelf().inSingletonScope().onActivation(({ container }, quickInputService: MonacoQuickInputService) => {
WebSocketConnectionProvider.createHandler(container, quickInputServicePath, quickInputService);
return quickInputService;
});
bind(QuickInputService).toService(MonacoQuickInputService);
bind(MonacoQuickAccessRegistry).toSelf().inSingletonScope();
bind(QuickAccessRegistry).toService(MonacoQuickAccessRegistry);
bind(GotoLineQuickAccessContribution).toSelf().inSingletonScope();
bind(QuickAccessContribution).toService(GotoLineQuickAccessContribution);
bind(GotoSymbolQuickAccessContribution).toSelf().inSingletonScope();
bind(QuickAccessContribution).toService(GotoSymbolQuickAccessContribution);
MonacoTextmateModuleBinder(bind, unbind, isBound, rebind);
bind(MonacoOutlineDecorator).toSelf().inSingletonScope();
bind(OutlineTreeDecorator).toService(MonacoOutlineDecorator);
bind(MonacoMimeService).toSelf().inSingletonScope();
rebind(MimeService).toService(MonacoMimeService);
bind(MonacoColorRegistry).toSelf().inSingletonScope();
rebind(ColorRegistry).toService(MonacoColorRegistry);
bind(ThemeServiceWithDB).toSelf().inSingletonScope();
rebind(ThemeService).toService(ThemeServiceWithDB);
bind(MonacoIconRegistry).toSelf().inSingletonScope();
bind(IconRegistry).toService(MonacoIconRegistry);
bind(FocusedMonacoUndoRedoHandler).toSelf().inSingletonScope();
bind(ActiveMonacoUndoRedoHandler).toSelf().inSingletonScope();
bind(UndoRedoHandler).toService(FocusedMonacoUndoRedoHandler);
bind(UndoRedoHandler).toService(ActiveMonacoUndoRedoHandler);
bind(DefaultContentHoverWidgetPatcher).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(DefaultContentHoverWidgetPatcher);
bind(MonacoDiffComputer).toSelf().inSingletonScope();
bind(DiffComputer).toService(MonacoDiffComputer);
bind(MonacoEditorContentMenuContribution).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).to(MonacoEditorContentMenuContribution);
});
export const MonacoConfigurationService = Symbol('MonacoConfigurationService');
export function createMonacoConfigurationService(container: interfaces.Container): IConfigurationService {
const preferences = container.get<PreferenceService>(PreferenceService);
const preferenceSchemaService = container.get<PreferenceSchemaService>(PreferenceSchemaService);
const service = new StandaloneConfigurationService(StandaloneServices.get(ILogService));
const _configuration: Configuration = service['_configuration'];
_configuration.getValue = (section, overrides) => {
const overrideIdentifier: string | undefined = (overrides && 'overrideIdentifier' in overrides && typeof overrides.overrideIdentifier === 'string')
? overrides['overrideIdentifier']
: undefined;
const resourceUri: string | undefined = (overrides && 'resource' in overrides && !!overrides['resource']) ? overrides['resource'].toString() : undefined;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const proxy = createPreferenceProxy<{ [key: string]: any }>(preferences, preferenceSchemaService.getJSONSchema(PreferenceScope.Folder), {
resourceUri, overrideIdentifier, style: 'both'
});
if (section) {
return proxy[section];
}
return proxy;
};
/*
* Since we never read values from the underlying service, writing to it doesn't make sense. The standalone editor writes to the configuration when being created,
* which makes sense in the standalone case where there is no preference infrastructure in place. Those writes degrade the performance, however, so we patch the
* service to an empty implementation.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
service.updateValues = (values: [string, any][]) => Promise.resolve();
/*
* There are a few places in Monaco where this method is called from, including actions for editor minimap in `ContextMenuController`.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
service.updateValue = (key: string, value: any) => preferences.updateValue(key, value);
const toTarget = (scope: PreferenceScope): ConfigurationTarget => {
switch (scope) {
case PreferenceScope.Default: return ConfigurationTarget.DEFAULT;
case PreferenceScope.User: return ConfigurationTarget.USER;
case PreferenceScope.Workspace: return ConfigurationTarget.WORKSPACE;
case PreferenceScope.Folder: return ConfigurationTarget.WORKSPACE_FOLDER;
}
};
interface FireDidChangeConfigurationContext {
changes: PreferenceChange[];
affectedKeys: Set<string>;
keys: Set<string>;
overrides: Map<string, Set<string>>
}
const newFireDidChangeConfigurationContext = (): FireDidChangeConfigurationContext => ({
changes: [],
affectedKeys: new Set<string>(),
keys: new Set<string>(),
overrides: new Map<string, Set<string>>()
});
const fireDidChangeConfiguration = (source: ConfigurationTarget, context: FireDidChangeConfigurationContext): void => {
if (!context.affectedKeys.size) {
return;
}
const overrides: [string, string[]][] = [];
for (const [override, values] of context.overrides) {
overrides.push([override, [...values]]);
}
service['_onDidChangeConfiguration'].fire(<IConfigurationChangeEvent>{
sourceConfig: {},
change: {
keys: [...context.keys],
overrides
},
affectedKeys: context.affectedKeys,
source,
affectsConfiguration: (prefix, options) => {
if (!context.affectedKeys.has(prefix)) {
return false;
}
for (const change of context.changes) {
const overridden = preferences.overriddenPreferenceName(change.preferenceName);
const preferenceName = overridden ? overridden.preferenceName : change.preferenceName;
if (preferenceName.startsWith(prefix)) {
if (options?.overrideIdentifier !== undefined) {
if (overridden && overridden.overrideIdentifier !== options?.overrideIdentifier) {
continue;
}
}
if (change.affects(options?.resource?.toString())) {
return true;
}
}
}
return false;
}
});
};
preferences.onPreferencesChanged(event => {
let source: ConfigurationTarget | undefined;
let context = newFireDidChangeConfigurationContext();
for (let key of Object.keys(event)) {
const change = event[key];
const target = toTarget(change.scope);
if (source !== undefined && target !== source) {
fireDidChangeConfiguration(source, context);
context = newFireDidChangeConfigurationContext();
}
context.changes.push(change);
source = target;
let overrideKeys: Set<string> | undefined;
if (key.startsWith('[')) {
const index = key.indexOf('.');
const override = key.substring(0, index);
const overrideIdentifier = override.match(OVERRIDE_PROPERTY_PATTERN)?.[1];
if (overrideIdentifier) {
context.keys.add(override);
context.affectedKeys.add(override);
overrideKeys = context.overrides.get(overrideIdentifier) || new Set<string>();
context.overrides.set(overrideIdentifier, overrideKeys);
key = key.substring(index + 1);
}
}
while (key) {
if (overrideKeys) {
overrideKeys.add(key);
}
context.keys.add(key);
context.affectedKeys.add(key);
const index = key.lastIndexOf('.');
key = key.substring(0, index);
}
}
if (source) {
fireDidChangeConfiguration(source, context);
}
});
return service;
}

View File

@@ -0,0 +1,47 @@
// *****************************************************************************
// Copyright (C) 2021 SAP SE or an SAP affiliate company and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { QuickAccessContribution } from '@theia/core/lib/browser/quick-input';
import { injectable } from '@theia/core/shared/inversify';
import { ICodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorBrowser';
import { ICodeEditorService } from '@theia/monaco-editor-core/esm/vs/editor/browser/services/codeEditorService';
import { StandaloneGotoLineQuickAccessProvider } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/quickAccess/standaloneGotoLineQuickAccess';
import { IQuickAccessRegistry, Extensions } from '@theia/monaco-editor-core/esm/vs/platform/quickinput/common/quickAccess';
import { Registry } from '@theia/monaco-editor-core/esm/vs/platform/registry/common/platform';
export class GotoLineQuickAccess extends StandaloneGotoLineQuickAccessProvider {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(...services: any[]);
constructor(@ICodeEditorService private readonly service: ICodeEditorService) {
super(service);
}
override get activeTextEditorControl(): ICodeEditor | undefined {
return (this.service.getFocusedCodeEditor() || this.service.getActiveCodeEditor()) ?? undefined;
}
}
@injectable()
export class GotoLineQuickAccessContribution implements QuickAccessContribution {
registerQuickAccessProvider(): void {
Registry.as<IQuickAccessRegistry>(Extensions.Quickaccess).registerQuickAccessProvider({
ctor: GotoLineQuickAccess,
prefix: ':',
placeholder: '',
helpEntries: [{ description: 'Go to line' }]
});
}
}

View File

@@ -0,0 +1,53 @@
// *****************************************************************************
// Copyright (C) 2021 SAP SE or an SAP affiliate company and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { QuickAccessContribution } from '@theia/core/lib/browser/quick-input';
import { injectable } from '@theia/core/shared/inversify';
import { ICodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorBrowser';
import { ICodeEditorService } from '@theia/monaco-editor-core/esm/vs/editor/browser/services/codeEditorService';
import { ILanguageFeaturesService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/languageFeatures';
import { IOutlineModelService } from '@theia/monaco-editor-core/esm/vs/editor/contrib/documentSymbols/browser/outlineModel';
import { StandaloneGotoSymbolQuickAccessProvider } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/quickAccess/standaloneGotoSymbolQuickAccess';
import { IQuickAccessRegistry, Extensions } from '@theia/monaco-editor-core/esm/vs/platform/quickinput/common/quickAccess';
import { Registry } from '@theia/monaco-editor-core/esm/vs/platform/registry/common/platform';
export class GotoSymbolQuickAccess extends StandaloneGotoSymbolQuickAccessProvider {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(...services: any[]);
constructor(
@ICodeEditorService protected readonly codeEditorService: ICodeEditorService,
@ILanguageFeaturesService protected readonly languageFeatures: ILanguageFeaturesService,
@IOutlineModelService protected readonly outlineService: IOutlineModelService,
) {
super(codeEditorService, languageFeatures, outlineService);
}
override get activeTextEditorControl(): ICodeEditor | undefined {
return (this.codeEditorService.getFocusedCodeEditor() ?? this.codeEditorService.getActiveCodeEditor()) ?? undefined;
}
}
@injectable()
export class GotoSymbolQuickAccessContribution implements QuickAccessContribution {
registerQuickAccessProvider(): void {
Registry.as<IQuickAccessRegistry>(Extensions.Quickaccess).registerQuickAccessProvider({
ctor: GotoSymbolQuickAccess,
prefix: '@',
placeholder: '',
helpEntries: [{ description: 'Go to symbol' }]
});
}
}

View File

@@ -0,0 +1,49 @@
// *****************************************************************************
// Copyright (C) 2023 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { IconRegistry } from '@theia/core/lib/browser/icon-registry';
import { getIconRegistry } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/iconRegistry';
import { IconDefinition, IconFontDefinition, ThemeIcon } from '@theia/core/lib/common/theme';
@injectable()
export class MonacoIconRegistry implements IconRegistry {
protected readonly iconRegistry = getIconRegistry();
registerIcon(id: string, defaults: ThemeIcon | IconDefinition, description?: string): ThemeIcon {
return this.iconRegistry.registerIcon(id, defaults, description);
}
deregisterIcon(id: string): void {
return this.iconRegistry.deregisterIcon(id);
}
registerIconFont(id: string, definition: IconFontDefinition): IconFontDefinition {
// need to cast because of vscode issue https://github.com/microsoft/vscode/issues/190584
return this.iconRegistry.registerIconFont(id, definition) as IconFontDefinition;
}
deregisterIconFont(id: string): void {
return this.iconRegistry.deregisterIconFont(id);
}
getIconFont(id: string): IconFontDefinition | undefined {
// need to cast because of vscode issue https://github.com/microsoft/vscode/issues/190584
return this.iconRegistry.getIconFont(id) as IconFontDefinition;
}
}

View File

@@ -0,0 +1,130 @@
// *****************************************************************************
// Copyright (C) 2020 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import * as idb from 'idb';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { ThemeService } from '@theia/core/lib/browser/theming';
import * as monaco from '@theia/monaco-editor-core';
import { injectable } from '@theia/core/shared/inversify';
import type { ThemeMix } from './textmate/monaco-theme-types';
import { Theme } from '@theia/core/lib/common/theme';
import { Emitter, Event, isObject } from '@theia/core';
let _monacoDB: Promise<idb.IDBPDatabase> | undefined;
if ('indexedDB' in window) {
_monacoDB = idb.openDB('theia-monaco', 1, {
upgrade: db => {
if (!db.objectStoreNames.contains('themes')) {
db.createObjectStore('themes', { keyPath: 'id' });
}
}
});
}
export const monacoDB = _monacoDB;
export interface MonacoThemeState {
id: string,
label: string,
description?: string,
uiTheme: monaco.editor.BuiltinTheme
data: ThemeMix
}
export namespace MonacoThemeState {
export function is(state: unknown): state is MonacoThemeState {
return isObject(state) && 'id' in state && 'label' in state && 'uiTheme' in state && 'data' in state;
}
}
export async function getThemes(): Promise<MonacoThemeState[]> {
if (!monacoDB) {
return [];
}
const db = await monacoDB;
const result = await db.transaction('themes', 'readonly').objectStore('themes').getAll();
return result.filter(MonacoThemeState.is);
}
export function putTheme(state: MonacoThemeState): Disposable {
const toDispose = new DisposableCollection(Disposable.create(() => { /* mark as not disposed */ }));
doPutTheme(state, toDispose);
return toDispose;
}
async function doPutTheme(state: MonacoThemeState, toDispose: DisposableCollection): Promise<void> {
if (!monacoDB) {
return;
}
const db = await monacoDB;
if (toDispose.disposed) {
return;
}
const id = state.id;
await db.transaction('themes', 'readwrite').objectStore('themes').put(state);
if (toDispose.disposed) {
await deleteTheme(id);
return;
}
toDispose.push(Disposable.create(() => deleteTheme(id)));
}
export async function deleteTheme(id: string): Promise<void> {
if (!monacoDB) {
return;
}
const db = await monacoDB;
await db.transaction('themes', 'readwrite').objectStore('themes').delete(id);
}
export function stateToTheme(state: MonacoThemeState): Theme {
const { id, label, description, uiTheme, data } = state;
const type = uiTheme === 'vs' ? 'light' : uiTheme === 'vs-dark' ? 'dark' : 'hc';
return {
type,
id,
label,
description,
editorTheme: data.name!
};
}
@injectable()
export class ThemeServiceWithDB extends ThemeService {
protected onDidRetrieveThemeEmitter = new Emitter<MonacoThemeState>();
get onDidRetrieveTheme(): Event<MonacoThemeState> {
return this.onDidRetrieveThemeEmitter.event;
}
override loadUserTheme(): void {
this.loadUserThemeWithDB();
}
protected async loadUserThemeWithDB(): Promise<void> {
const themeId = window.localStorage.getItem(ThemeService.STORAGE_KEY) ?? this.defaultTheme.id;
const theme = this.themes[themeId] ?? await getThemes().then(themes => {
const matchingTheme = themes.find(candidate => candidate.id === themeId);
if (matchingTheme) {
this.onDidRetrieveThemeEmitter.fire(matchingTheme);
return stateToTheme(matchingTheme);
}
}) ?? this.getTheme(themeId);
// In case the theme comes from the DB.
if (!this.themes[theme.id]) {
this.themes[theme.id] = theme;
}
this.setCurrentTheme(theme.id, false);
this.deferredInitializer.resolve();
}
}

View File

@@ -0,0 +1,171 @@
// *****************************************************************************
// Copyright (C) 2023 STMicroelectronics and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
/*
* The code in this file is responsible for overriding service implementations in the Monaco editor with our own Theia-based implementations.
* Since we only get a single chance to call `StandaloneServices.initialize()` with our overrides, we need to make sure that initialize is called before the first call to
* `StandaloneServices.get()` or `StandaloneServices.initialize()`. As we do not control the mechanics of Inversify instance constructions, the approach here is to call
* `MonacoInit.init()` from the `index.js` file after all container modules are loaded, but before the first object is fetched from it.
* `StandaloneServices.initialize()` is called with service descriptors, not service instances. This lets us finish all overrides before any inversify object is constructed and
* might call `initialize()` while being constructed.
* The service descriptors require a constructor function, so we declare dummy class for each Monaco service we override. But instead of returning an instance of the dummy class,
* we fetch the implementation of the monaco service from the inversify container.
* The inversify-constructed services must not call StandaloneServices.get() or StandaloneServices.initialize() from their constructors. Calling `get`()` in postConstruct methods
* is allowed.
*/
// Before importing anything from monaco we need to override its localization function
import * as MonacoNls from '@theia/monaco-editor-core/esm/vs/nls';
import { nls } from '@theia/core/lib/common/nls';
import { FormatType, Localization } from '@theia/core/lib/common/i18n/localization';
function localize(label: string, ...args: FormatType[]): MonacoNls.ILocalizedString {
const original = Localization.format(label, args);
if (nls.locale) {
const defaultKey = nls.getDefaultKey(label);
if (defaultKey) {
return {
original,
value: nls.localize(defaultKey, label, ...args)
};
}
}
return {
original,
value: original
};
}
Object.assign(MonacoNls, {
localize(_key: string, label: string, ...args: FormatType[]): string {
return localize(label, ...args).value;
},
localize2(_key: string, label: string, ...args: FormatType[]): MonacoNls.ILocalizedString {
return localize(label, ...args);
}
});
import { Container } from '@theia/core/shared/inversify';
import { ICodeEditorService } from '@theia/monaco-editor-core/esm/vs/editor/browser/services/codeEditorService';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
import { SyncDescriptor } from '@theia/monaco-editor-core/esm/vs/platform/instantiation/common/descriptors';
import { MonacoEditorServiceFactory, MonacoEditorServiceFactoryType } from './monaco-editor-service';
import { IConfigurationService } from '@theia/monaco-editor-core/esm/vs/platform/configuration/common/configuration';
import { ITextModelService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/resolverService';
import { MonacoConfigurationService } from './monaco-frontend-module';
import { MonacoTextModelService } from './monaco-text-model-service';
import { MonacoContextMenuService } from './monaco-context-menu';
import { IContextMenuService } from '@theia/monaco-editor-core/esm/vs/platform/contextview/browser/contextView';
import { IContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/common/contextkey';
import { IThemeService } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/themeService';
import { MonacoBulkEditService } from './monaco-bulk-edit-service';
import { MonacoCommandService } from './monaco-command-service';
import { IBulkEditService } from '@theia/monaco-editor-core/esm/vs/editor/browser/services/bulkEditService';
import { ICommandService } from '@theia/monaco-editor-core/esm/vs/platform/commands/common/commands';
import { MonacoQuickInputImplementation } from './monaco-quick-input-service';
import { IQuickInputService } from '@theia/monaco-editor-core/esm/vs/platform/quickinput/common/quickInput';
import { IStandaloneThemeService } from '@theia/monaco-editor-core/esm/vs/editor/standalone/common/standaloneTheme';
import { MonacoStandaloneThemeService } from './monaco-standalone-theme-service';
import { createContentHoverWidgetPatcher } from './content-hover-widget-patcher';
import { IHoverService } from '@theia/monaco-editor-core/esm/vs/platform/hover/browser/hover';
import { setBaseLayerHoverDelegate } from '@theia/monaco-editor-core/esm/vs/base/browser/ui/hover/hoverDelegate2';
import { IWorkspaceContextService } from '@theia/monaco-editor-core/esm/vs/platform/workspace/common/workspace';
import { MonacoWorkspaceContextService } from './monaco-workspace-context-service';
export const contentHoverWidgetPatcher = createContentHoverWidgetPatcher();
class MonacoEditorServiceConstructor {
/**
* MonacoEditorService needs other Monaco services as constructor parameters, so we need to do use a factory for constructing the service. If we want the singleton instance,
* we need to fetch it from the `StandaloneServices` class instead of injecting it.
* @param container
* @param contextKeyService
* @param themeService
*/
constructor(container: Container,
@IContextKeyService contextKeyService: IContextKeyService,
@IThemeService themeService: IThemeService) {
return container.get<MonacoEditorServiceFactoryType>(MonacoEditorServiceFactory)(contextKeyService, themeService);
};
}
class MonacoConfigurationServiceConstructor {
constructor(container: Container) {
return container.get(MonacoConfigurationService);
}
}
class MonacoTextModelServiceConstructor {
constructor(container: Container) {
return container.get(MonacoTextModelService);
}
}
class MonacoContextMenuServiceConstructor {
constructor(container: Container) {
return container.get(MonacoContextMenuService);
}
}
class MonacoBulkEditServiceConstructor {
constructor(container: Container) {
return container.get(MonacoBulkEditService);
}
}
class MonacoCommandServiceConstructor {
constructor(container: Container) {
return container.get(MonacoCommandService);
}
}
class MonacoQuickInputImplementationConstructor {
constructor(container: Container) {
return container.get(MonacoQuickInputImplementation);
}
}
class MonacoStandaloneThemeServiceConstructor {
constructor(container: Container) {
return new MonacoStandaloneThemeService();
}
}
class MonacoWorkspaceContextServiceConstructor {
constructor(container: Container) {
return container.get(MonacoWorkspaceContextService);
}
}
export namespace MonacoInit {
export function init(container: Container): void {
StandaloneServices.initialize({
[ICodeEditorService.toString()]: new SyncDescriptor(MonacoEditorServiceConstructor, [container]),
[IConfigurationService.toString()]: new SyncDescriptor(MonacoConfigurationServiceConstructor, [container]),
[ITextModelService.toString()]: new SyncDescriptor(MonacoTextModelServiceConstructor, [container]),
[IContextMenuService.toString()]: new SyncDescriptor(MonacoContextMenuServiceConstructor, [container]),
[IBulkEditService.toString()]: new SyncDescriptor(MonacoBulkEditServiceConstructor, [container]),
[ICommandService.toString()]: new SyncDescriptor(MonacoCommandServiceConstructor, [container]),
[IQuickInputService.toString()]: new SyncDescriptor(MonacoQuickInputImplementationConstructor, [container]),
[IStandaloneThemeService.toString()]: new SyncDescriptor(MonacoStandaloneThemeServiceConstructor, []),
[IWorkspaceContextService.toString()]: new SyncDescriptor(MonacoWorkspaceContextServiceConstructor, [container])
});
// Make sure the global base hover delegate is initialized as otherwise the quick input will throw an error and not update correctly
// in case no Monaco editor was constructed before and items with keybindings are shown. See #15042.
setBaseLayerHoverDelegate(StandaloneServices.get(IHoverService));
}
}

View File

@@ -0,0 +1,111 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { KeybindingContribution, KeybindingRegistry, KeybindingScope, KeyCode } from '@theia/core/lib/browser';
import { MonacoCommands } from './monaco-command';
import { MonacoCommandRegistry } from './monaco-command-registry';
import { CommandRegistry, DisposableCollection, environment, isOSX } from '@theia/core';
import { MonacoResolvedKeybinding } from './monaco-resolved-keybinding';
import { KeybindingsRegistry } from '@theia/monaco-editor-core/esm/vs/platform/keybinding/common/keybindingsRegistry';
import { StandaloneKeybindingService, StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
import { IKeybindingService } from '@theia/monaco-editor-core/esm/vs/platform/keybinding/common/keybinding';
import { MonacoContextKeyService } from './monaco-context-key-service';
import { KEY_CODE_MAP } from './monaco-keycode-map';
import * as monaco from '@theia/monaco-editor-core';
@injectable()
export class MonacoKeybindingContribution implements KeybindingContribution {
protected toDisposeOnKeybindingChange = new DisposableCollection();
@inject(MonacoCommandRegistry) protected readonly commands: MonacoCommandRegistry;
@inject(KeybindingRegistry) protected readonly keybindings: KeybindingRegistry;
@inject(CommandRegistry) protected readonly theiaCommandRegistry: CommandRegistry;
@inject(MonacoContextKeyService) protected readonly contextKeyService: MonacoContextKeyService;
@postConstruct()
protected init(): void {
this.keybindings.onKeybindingsChanged(() => this.updateMonacoKeybindings());
}
registerKeybindings(registry: KeybindingRegistry): void {
const defaultKeybindings = KeybindingsRegistry.getDefaultKeybindings();
for (const item of defaultKeybindings) {
const command = this.commands.validate(item.command || undefined);
if (command && item.keybinding) {
const when = (item.when && item.when.serialize()) ?? undefined;
let keybinding;
if (item.command === MonacoCommands.GO_TO_DEFINITION && !environment.electron.is()) {
keybinding = 'ctrlcmd+f11';
} else {
keybinding = MonacoResolvedKeybinding.toKeybinding(item.keybinding.chords);
}
registry.registerKeybinding({ command, keybinding, when });
}
}
}
protected updateMonacoKeybindings(): void {
const monacoKeybindingRegistry = StandaloneServices.get(IKeybindingService);
if (monacoKeybindingRegistry instanceof StandaloneKeybindingService) {
this.toDisposeOnKeybindingChange.dispose();
for (const binding of this.keybindings.getKeybindingsByScope(KeybindingScope.USER).concat(this.keybindings.getKeybindingsByScope(KeybindingScope.WORKSPACE))) {
const resolved = this.keybindings.resolveKeybinding(binding);
const command = binding.command;
const when = binding.when
? this.contextKeyService.parse(binding.when)
: binding.context
? this.contextKeyService.parse(binding.context)
: undefined;
this.toDisposeOnKeybindingChange.push(monacoKeybindingRegistry.addDynamicKeybinding(
binding.command,
this.toMonacoKeybindingNumber(resolved),
(_, ...args) => this.theiaCommandRegistry.executeCommand(command, ...args),
when,
));
}
}
}
protected toMonacoKeybindingNumber(codes: KeyCode[]): number {
const [firstPart, secondPart] = codes;
if (codes.length > 2) {
console.warn('Key chords should not consist of more than two parts; got ', codes);
}
const encodedFirstPart = this.toSingleMonacoKeybindingNumber(firstPart);
const encodedSecondPart = secondPart ? this.toSingleMonacoKeybindingNumber(secondPart) << 16 : 0;
return monaco.KeyMod.chord(encodedFirstPart, encodedSecondPart);
}
protected toSingleMonacoKeybindingNumber(code: KeyCode): number {
const keyCode = code.key?.keyCode !== undefined ? KEY_CODE_MAP[code.key.keyCode] : 0;
let encoded = (keyCode >>> 0) & 0x000000FF;
if (code.alt) {
encoded |= monaco.KeyMod.Alt;
}
if (code.shift) {
encoded |= monaco.KeyMod.Shift;
}
if (code.ctrl) {
encoded |= monaco.KeyMod.WinCtrl;
}
if (code.meta && isOSX) {
encoded |= monaco.KeyMod.CtrlCmd;
}
return encoded;
}
}

View File

@@ -0,0 +1,171 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as browser from '@theia/core/lib/browser';
// This is exported as part of the public API, but we use it with private API's so we need to refer to the private version.
import { KeyCode } from '@theia/monaco-editor-core/esm/vs/base/common/keyCodes';
import * as MonacoPlatform from '@theia/monaco-editor-core/esm/vs/base/common/platform';
export const KEY_CODE_MAP: KeyCode[] = [];
(function (): void {
KEY_CODE_MAP[3] = KeyCode.PauseBreak; // VK_CANCEL 0x03 Control-break processing
KEY_CODE_MAP[8] = KeyCode.Backspace;
KEY_CODE_MAP[9] = KeyCode.Tab;
KEY_CODE_MAP[13] = KeyCode.Enter;
KEY_CODE_MAP[16] = KeyCode.Shift;
KEY_CODE_MAP[17] = KeyCode.Ctrl;
KEY_CODE_MAP[18] = KeyCode.Alt;
KEY_CODE_MAP[19] = KeyCode.PauseBreak;
KEY_CODE_MAP[20] = KeyCode.CapsLock;
KEY_CODE_MAP[27] = KeyCode.Escape;
KEY_CODE_MAP[32] = KeyCode.Space;
KEY_CODE_MAP[33] = KeyCode.PageUp;
KEY_CODE_MAP[34] = KeyCode.PageDown;
KEY_CODE_MAP[35] = KeyCode.End;
KEY_CODE_MAP[36] = KeyCode.Home;
KEY_CODE_MAP[37] = KeyCode.LeftArrow;
KEY_CODE_MAP[38] = KeyCode.UpArrow;
KEY_CODE_MAP[39] = KeyCode.RightArrow;
KEY_CODE_MAP[40] = KeyCode.DownArrow;
KEY_CODE_MAP[45] = KeyCode.Insert;
KEY_CODE_MAP[46] = KeyCode.Delete;
KEY_CODE_MAP[48] = KeyCode.Digit0;
KEY_CODE_MAP[49] = KeyCode.Digit1;
KEY_CODE_MAP[50] = KeyCode.Digit2;
KEY_CODE_MAP[51] = KeyCode.Digit3;
KEY_CODE_MAP[52] = KeyCode.Digit4;
KEY_CODE_MAP[53] = KeyCode.Digit5;
KEY_CODE_MAP[54] = KeyCode.Digit6;
KEY_CODE_MAP[55] = KeyCode.Digit7;
KEY_CODE_MAP[56] = KeyCode.Digit8;
KEY_CODE_MAP[57] = KeyCode.Digit9;
KEY_CODE_MAP[65] = KeyCode.KeyA;
KEY_CODE_MAP[66] = KeyCode.KeyB;
KEY_CODE_MAP[67] = KeyCode.KeyC;
KEY_CODE_MAP[68] = KeyCode.KeyD;
KEY_CODE_MAP[69] = KeyCode.KeyE;
KEY_CODE_MAP[70] = KeyCode.KeyF;
KEY_CODE_MAP[71] = KeyCode.KeyG;
KEY_CODE_MAP[72] = KeyCode.KeyH;
KEY_CODE_MAP[73] = KeyCode.KeyI;
KEY_CODE_MAP[74] = KeyCode.KeyJ;
KEY_CODE_MAP[75] = KeyCode.KeyK;
KEY_CODE_MAP[76] = KeyCode.KeyL;
KEY_CODE_MAP[77] = KeyCode.KeyM;
KEY_CODE_MAP[78] = KeyCode.KeyN;
KEY_CODE_MAP[79] = KeyCode.KeyO;
KEY_CODE_MAP[80] = KeyCode.KeyP;
KEY_CODE_MAP[81] = KeyCode.KeyQ;
KEY_CODE_MAP[82] = KeyCode.KeyR;
KEY_CODE_MAP[83] = KeyCode.KeyS;
KEY_CODE_MAP[84] = KeyCode.KeyT;
KEY_CODE_MAP[85] = KeyCode.KeyU;
KEY_CODE_MAP[86] = KeyCode.KeyV;
KEY_CODE_MAP[87] = KeyCode.KeyW;
KEY_CODE_MAP[88] = KeyCode.KeyX;
KEY_CODE_MAP[89] = KeyCode.KeyY;
KEY_CODE_MAP[90] = KeyCode.KeyZ;
KEY_CODE_MAP[93] = KeyCode.ContextMenu;
KEY_CODE_MAP[96] = KeyCode.Numpad0;
KEY_CODE_MAP[97] = KeyCode.Numpad1;
KEY_CODE_MAP[98] = KeyCode.Numpad2;
KEY_CODE_MAP[99] = KeyCode.Numpad3;
KEY_CODE_MAP[100] = KeyCode.Numpad4;
KEY_CODE_MAP[101] = KeyCode.Numpad5;
KEY_CODE_MAP[102] = KeyCode.Numpad6;
KEY_CODE_MAP[103] = KeyCode.Numpad7;
KEY_CODE_MAP[104] = KeyCode.Numpad8;
KEY_CODE_MAP[105] = KeyCode.Numpad9;
KEY_CODE_MAP[106] = KeyCode.NumpadMultiply;
KEY_CODE_MAP[107] = KeyCode.NumpadAdd;
KEY_CODE_MAP[108] = KeyCode.NUMPAD_SEPARATOR;
KEY_CODE_MAP[109] = KeyCode.NumpadSubtract;
KEY_CODE_MAP[110] = KeyCode.NumpadDecimal;
KEY_CODE_MAP[111] = KeyCode.NumpadDivide;
KEY_CODE_MAP[112] = KeyCode.F1;
KEY_CODE_MAP[113] = KeyCode.F2;
KEY_CODE_MAP[114] = KeyCode.F3;
KEY_CODE_MAP[115] = KeyCode.F4;
KEY_CODE_MAP[116] = KeyCode.F5;
KEY_CODE_MAP[117] = KeyCode.F6;
KEY_CODE_MAP[118] = KeyCode.F7;
KEY_CODE_MAP[119] = KeyCode.F8;
KEY_CODE_MAP[120] = KeyCode.F9;
KEY_CODE_MAP[121] = KeyCode.F10;
KEY_CODE_MAP[122] = KeyCode.F11;
KEY_CODE_MAP[123] = KeyCode.F12;
KEY_CODE_MAP[124] = KeyCode.F13;
KEY_CODE_MAP[125] = KeyCode.F14;
KEY_CODE_MAP[126] = KeyCode.F15;
KEY_CODE_MAP[127] = KeyCode.F16;
KEY_CODE_MAP[128] = KeyCode.F17;
KEY_CODE_MAP[129] = KeyCode.F18;
KEY_CODE_MAP[130] = KeyCode.F19;
KEY_CODE_MAP[144] = KeyCode.NumLock;
KEY_CODE_MAP[145] = KeyCode.ScrollLock;
KEY_CODE_MAP[186] = KeyCode.Semicolon;
KEY_CODE_MAP[187] = KeyCode.Equal;
KEY_CODE_MAP[188] = KeyCode.Comma;
KEY_CODE_MAP[189] = KeyCode.Minus;
KEY_CODE_MAP[190] = KeyCode.Period;
KEY_CODE_MAP[191] = KeyCode.Slash;
KEY_CODE_MAP[192] = KeyCode.Backquote;
KEY_CODE_MAP[193] = KeyCode.ABNT_C1;
KEY_CODE_MAP[194] = KeyCode.ABNT_C2;
KEY_CODE_MAP[219] = KeyCode.BracketLeft;
KEY_CODE_MAP[220] = KeyCode.Backslash;
KEY_CODE_MAP[221] = KeyCode.BracketRight;
KEY_CODE_MAP[222] = KeyCode.Quote;
KEY_CODE_MAP[223] = KeyCode.OEM_8;
KEY_CODE_MAP[226] = KeyCode.IntlBackslash;
/**
* https://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html
* If an Input Method Editor is processing key input and the event is keydown, return 229.
*/
KEY_CODE_MAP[229] = KeyCode.KEY_IN_COMPOSITION;
if (browser.isIE) {
KEY_CODE_MAP[91] = KeyCode.Meta;
} else if (browser.isFirefox) {
KEY_CODE_MAP[59] = KeyCode.Semicolon;
KEY_CODE_MAP[107] = KeyCode.Equal;
KEY_CODE_MAP[109] = KeyCode.Minus;
if (MonacoPlatform.OS === MonacoPlatform.OperatingSystem.Macintosh) {
KEY_CODE_MAP[224] = KeyCode.Meta;
}
} else if (browser.isWebKit) {
KEY_CODE_MAP[91] = KeyCode.Meta;
if (MonacoPlatform.OS === MonacoPlatform.OperatingSystem.Macintosh) {
// the two meta keys in the Mac have different key codes (91 and 93)
KEY_CODE_MAP[93] = KeyCode.Meta;
} else {
KEY_CODE_MAP[92] = KeyCode.Meta;
}
}
})();

View File

@@ -0,0 +1,177 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { SymbolInformation, WorkspaceSymbolParams } from '@theia/core/shared/vscode-languageserver-protocol';
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { ProblemManager } from '@theia/markers/lib/browser/problem/problem-manager';
import URI from '@theia/core/lib/common/uri';
import { MaybePromise, Mutable } from '@theia/core/lib/common/types';
import { Disposable } from '@theia/core/lib/common/disposable';
import { CancellationToken } from '@theia/core/lib/common/cancellation';
import { Language, LanguageService } from '@theia/core/lib/browser/language-service';
import { MonacoMarkerCollection } from './monaco-marker-collection';
import { ProtocolToMonacoConverter } from './protocol-to-monaco-converter';
import * as monaco from '@theia/monaco-editor-core';
import { FileStat } from '@theia/filesystem/lib/common/files';
import { FileStatNode } from '@theia/filesystem/lib/browser';
import { ILanguageService } from '@theia/monaco-editor-core/esm/vs/editor/common/languages/language';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
export interface WorkspaceSymbolProvider {
provideWorkspaceSymbols(params: WorkspaceSymbolParams, token: CancellationToken): MaybePromise<SymbolInformation[] | undefined>;
resolveWorkspaceSymbol?(symbol: SymbolInformation, token: CancellationToken): Thenable<SymbolInformation | undefined>
}
@injectable()
export class MonacoLanguages extends LanguageService {
readonly workspaceSymbolProviders: WorkspaceSymbolProvider[] = [];
protected readonly markers = new Map<string, MonacoMarkerCollection>();
protected readonly icons = new Map<string, string>();
@inject(ProblemManager) protected readonly problemManager: ProblemManager;
@inject(ProtocolToMonacoConverter) protected readonly p2m: ProtocolToMonacoConverter;
@postConstruct()
protected init(): void {
this.problemManager.onDidChangeMarkers(uri => this.updateMarkers(uri));
monaco.editor.onDidCreateModel(model => this.updateModelMarkers(model));
}
updateMarkers(uri: URI): void {
const markers = this.problemManager.findMarkers({ uri });
const uriString = uri.toString();
const collection = this.markers.get(uriString) || new MonacoMarkerCollection(uri, this.p2m);
this.markers.set(uriString, collection);
collection.updateMarkers(markers);
}
updateModelMarkers(model: monaco.editor.ITextModel): void {
const uriString = model.uri.toString();
const uri = new URI(uriString);
const collection = this.markers.get(uriString) || new MonacoMarkerCollection(uri, this.p2m);
this.markers.set(uriString, collection);
collection.updateModelMarkers(model);
}
registerWorkspaceSymbolProvider(provider: WorkspaceSymbolProvider): Disposable {
this.workspaceSymbolProviders.push(provider);
return Disposable.create(() => {
const index = this.workspaceSymbolProviders.indexOf(provider);
if (index !== -1) {
this.workspaceSymbolProviders.splice(index, 1);
}
});
}
override get languages(): Language[] {
return [...this.mergeLanguages(monaco.languages.getLanguages()).values()];
}
override getLanguage(languageId: string): Language | undefined {
return this.mergeLanguages(monaco.languages.getLanguages().filter(language => language.id === languageId)).get(languageId);
}
override detectLanguage(obj: unknown): Language | undefined {
if (obj === undefined) {
return undefined;
}
if (typeof obj === 'string') {
return this.detectLanguageByIdOrName(obj) ?? this.detectLanguageByURI(new URI(obj));
}
if (obj instanceof URI) {
return this.detectLanguageByURI(obj);
}
if (FileStat.is(obj)) {
return this.detectLanguageByURI(obj.resource);
}
if (FileStatNode.is(obj)) {
return this.detectLanguageByURI(obj.uri);
}
return undefined;
}
protected detectLanguageByIdOrName(obj: string): Language | undefined {
const languageById = this.getLanguage(obj);
if (languageById) {
return languageById;
}
const languageId = this.getLanguageIdByLanguageName(obj);
return languageId ? this.getLanguage(languageId) : undefined;
}
protected detectLanguageByURI(uri: URI): Language | undefined {
const languageId = StandaloneServices.get(ILanguageService).guessLanguageIdByFilepathOrFirstLine(uri['codeUri']);
return languageId ? this.getLanguage(languageId) : undefined;
}
getExtension(languageId: string): string | undefined {
return this.getLanguage(languageId)?.extensions.values().next().value;
}
override registerIcon(languageId: string, iconClass: string): Disposable {
this.icons.set(languageId, iconClass);
this.onDidChangeIconEmitter.fire({ languageId });
return Disposable.create(() => {
this.icons.delete(languageId);
this.onDidChangeIconEmitter.fire({ languageId });
});
}
override getIcon(obj: unknown): string | undefined {
const language = this.detectLanguage(obj);
return language ? this.icons.get(language.id) : undefined;
}
getLanguageIdByLanguageName(languageName: string): string | undefined {
return monaco.languages.getLanguages().find(language => language.aliases?.includes(languageName))?.id;
}
protected mergeLanguages(registered: monaco.languages.ILanguageExtensionPoint[]): Map<string, Mutable<Language>> {
const languages = new Map<string, Mutable<Language>>();
for (const { id, aliases, extensions, filenames } of registered) {
const merged = languages.get(id) || {
id,
name: '',
extensions: new Set(),
filenames: new Set()
};
if (!merged.name && aliases && aliases.length) {
merged.name = aliases[0];
}
if (extensions && extensions.length) {
for (const extension of extensions) {
merged.extensions.add(extension);
}
}
if (filenames && filenames.length) {
for (const filename of filenames) {
merged.filenames.add(filename);
}
}
languages.set(id, merged);
}
for (const [id, language] of languages) {
if (!language.name) {
language.name = id;
}
}
return languages;
}
}

View File

@@ -0,0 +1,83 @@
// *****************************************************************************
// Copyright (C) 2020 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { Diagnostic } from '@theia/core/shared/vscode-languageserver-protocol';
import { ProtocolToMonacoConverter } from './protocol-to-monaco-converter';
import * as monaco from '@theia/monaco-editor-core';
import { Marker } from '@theia/markers/lib/common/marker';
import URI from '@theia/core/lib/common/uri';
export class MonacoMarkerCollection {
protected readonly uri: monaco.Uri;
protected readonly p2m: ProtocolToMonacoConverter;
protected markers: Marker<Diagnostic>[] = [];
protected owners = new Map<string, Diagnostic[]>();
protected didUpdate: boolean = false;
constructor(
uri: URI,
p2m: ProtocolToMonacoConverter
) {
this.uri = monaco.Uri.parse(uri.toString());
this.p2m = p2m;
}
updateMarkers(markers: Marker<Diagnostic>[]): void {
this.markers = markers;
const model = monaco.editor.getModel(this.uri);
this.doUpdateMarkers(model ? model : undefined);
}
updateModelMarkers(model: monaco.editor.ITextModel): void {
if (!this.didUpdate) {
this.doUpdateMarkers(model);
return;
}
for (const [owner, diagnostics] of this.owners) {
this.setModelMarkers(model, owner, diagnostics);
}
}
doUpdateMarkers(model: monaco.editor.ITextModel | undefined): void {
if (!model) {
this.didUpdate = false;
return;
}
this.didUpdate = true;
const toClean = new Set<string>(this.owners.keys());
this.owners.clear();
for (const marker of this.markers) {
const diagnostics = this.owners.get(marker.owner) || [];
diagnostics.push(marker.data);
this.owners.set(marker.owner, diagnostics);
}
for (const [owner, diagnostics] of this.owners) {
toClean.delete(owner);
this.setModelMarkers(model, owner, diagnostics);
}
for (const owner of toClean) {
this.clearModelMarkers(model, owner);
}
}
setModelMarkers(model: monaco.editor.ITextModel, owner: string, diagnostics: Diagnostic[]): void {
monaco.editor.setModelMarkers(model, owner, this.p2m.asDiagnostics(diagnostics));
}
clearModelMarkers(model: monaco.editor.ITextModel, owner: string): void {
monaco.editor.setModelMarkers(model, owner, []);
}
}

View File

@@ -0,0 +1,148 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { MAIN_MENU_BAR, MenuAction, MenuContribution, MenuModelRegistry, MenuPath } from '@theia/core/lib/common';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import { EDITOR_CONTEXT_MENU, EditorMainMenu } from '@theia/editor/lib/browser';
import { IMenuItem, MenuId, MenuRegistry, isIMenuItem } from '@theia/monaco-editor-core/esm/vs/platform/actions/common/actions';
import { MonacoCommands } from './monaco-command';
import { MonacoCommandRegistry } from './monaco-command-registry';
export interface MonacoActionGroup {
id: string;
actions: string[];
}
export namespace MonacoMenus {
export const SELECTION = [...MAIN_MENU_BAR, '3_selection'];
export const PEEK_CONTEXT_SUBMENU: MenuPath = [...EDITOR_CONTEXT_MENU, 'navigation', 'peek_submenu'];
export const MARKERS_GROUP = [...EditorMainMenu.GO, '5_markers_group'];
}
@injectable()
export class MonacoEditorMenuContribution implements MenuContribution {
constructor(
@inject(MonacoCommandRegistry) protected readonly commands: MonacoCommandRegistry
) { }
registerMenus(registry: MenuModelRegistry): void {
registry.registerSubmenu(EDITOR_CONTEXT_MENU, 'Editor Context Menu');
for (const item of MenuRegistry.getMenuItems(MenuId.EditorContext)) {
if (!isIMenuItem(item)) {
continue;
}
const commandId = this.commands.validate(item.command.id);
if (commandId) {
const nodeId = MonacoCommands.COMMON_ACTIONS.get(commandId) || commandId;
const menuPath = item.group ? [...EDITOR_CONTEXT_MENU, item.group] : EDITOR_CONTEXT_MENU;
if (!registry.getMenuNode([...menuPath, nodeId])) {
// Don't add additional actions if the item is already registered.
registry.registerMenuAction(menuPath, this.buildMenuAction(commandId, item));
}
}
}
this.registerPeekSubmenu(registry);
registry.registerSubmenu(MonacoMenus.SELECTION, nls.localizeByDefault('Selection'));
for (const item of MenuRegistry.getMenuItems(MenuId.MenubarSelectionMenu)) {
if (!isIMenuItem(item)) {
continue;
}
const commandId = this.commands.validate(item.command.id);
if (commandId) {
const menuPath = [...MonacoMenus.SELECTION, (item.group || '')];
registry.registerMenuAction(menuPath, this.buildMenuAction(commandId, item));
}
}
// Builtin monaco language features commands.
registry.registerMenuAction(EditorMainMenu.LANGUAGE_FEATURES_GROUP, {
commandId: 'editor.action.quickOutline',
label: nls.localizeByDefault('Go to Symbol in Editor...'),
order: '1'
});
registry.registerMenuAction(EditorMainMenu.LANGUAGE_FEATURES_GROUP, {
commandId: 'editor.action.revealDefinition',
label: nls.localizeByDefault('Go to Definition'),
order: '2'
});
registry.registerMenuAction(EditorMainMenu.LANGUAGE_FEATURES_GROUP, {
commandId: 'editor.action.revealDeclaration',
label: nls.localizeByDefault('Go to Declaration'),
order: '3'
});
registry.registerMenuAction(EditorMainMenu.LANGUAGE_FEATURES_GROUP, {
commandId: 'editor.action.goToTypeDefinition',
label: nls.localizeByDefault('Go to Type Definition'),
order: '4'
});
registry.registerMenuAction(EditorMainMenu.LANGUAGE_FEATURES_GROUP, {
commandId: 'editor.action.goToImplementation',
label: nls.localizeByDefault('Go to Implementations'),
order: '5'
});
registry.registerMenuAction(EditorMainMenu.LANGUAGE_FEATURES_GROUP, {
commandId: 'editor.action.goToReferences',
label: nls.localizeByDefault('Go to References'),
order: '6'
});
registry.registerMenuAction(EditorMainMenu.LOCATION_GROUP, {
commandId: 'editor.action.jumpToBracket',
label: nls.localizeByDefault('Go to Bracket'),
order: '2'
});
// Builtin monaco problem commands.
registry.registerMenuAction(MonacoMenus.MARKERS_GROUP, {
commandId: 'editor.action.marker.nextInFiles',
label: nls.localizeByDefault('Next Problem'),
order: '1'
});
registry.registerMenuAction(MonacoMenus.MARKERS_GROUP, {
commandId: 'editor.action.marker.prevInFiles',
label: nls.localizeByDefault('Previous Problem'),
order: '2'
});
}
protected registerPeekSubmenu(registry: MenuModelRegistry): void {
registry.registerSubmenu(MonacoMenus.PEEK_CONTEXT_SUBMENU, nls.localizeByDefault('Peek'));
for (const item of MenuRegistry.getMenuItems(MenuId.EditorContextPeek)) {
if (!isIMenuItem(item)) {
continue;
}
const commandId = this.commands.validate(item.command.id);
if (commandId) {
registry.registerMenuAction([...MonacoMenus.PEEK_CONTEXT_SUBMENU, item.group || ''], this.buildMenuAction(commandId, item));
}
}
}
protected buildMenuAction(commandId: string, item: IMenuItem): MenuAction {
const title = typeof item.command.title === 'string' ? item.command.title : item.command.title.value;
const label = this.removeMnemonic(title);
const order = item.order ? String(item.order) : '';
return { commandId, order, label };
}
protected removeMnemonic(label: string): string {
return label.replace(/\(&&\w\)|&&/g, '');
}
}

View File

@@ -0,0 +1,71 @@
// *****************************************************************************
// Copyright (C) 2019 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import debounce = require('@theia/core/shared/lodash.debounce');
import { injectable } from '@theia/core/shared/inversify';
import { MimeAssociation, MimeService } from '@theia/core/lib/browser/mime-service';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
import { ILanguageService } from '@theia/monaco-editor-core/esm/vs/editor/common/languages/language';
import * as monaco from '@theia/monaco-editor-core';
import { clearConfiguredLanguageAssociations, registerConfiguredLanguageAssociation } from '@theia/monaco-editor-core/esm/vs/editor/common/services/languagesAssociations';
import { LanguageService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/languageService';
@injectable()
export class MonacoMimeService extends MimeService {
protected associations: MimeAssociation[] = [];
protected updatingAssociations = false;
constructor() {
super();
StandaloneServices.get(ILanguageService).onDidChange(() => {
if (this.updatingAssociations) {
return;
}
this.updateAssociations();
});
}
override setAssociations(associations: MimeAssociation[]): void {
this.associations = associations;
this.updateAssociations();
}
protected updateAssociations = debounce(() => {
this.updatingAssociations = true;
try {
clearConfiguredLanguageAssociations();
for (const association of this.associations) {
const mimetype = this.getMimeForMode(association.id) || `text/x-${association.id}`;
registerConfiguredLanguageAssociation({ id: association.id, mime: mimetype, filepattern: association.filepattern });
}
(StandaloneServices.get(ILanguageService) as LanguageService)['_onDidChange'].fire(undefined);
} finally {
this.updatingAssociations = false;
}
});
protected getMimeForMode(langId: string): string | undefined {
for (const language of monaco.languages.getLanguages()) {
if (language.id === langId && language.mimetypes) {
return language.mimetypes[0];
}
}
return undefined;
}
}

View File

@@ -0,0 +1,404 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject } from '@theia/core/shared/inversify';
import { FrontendApplicationContribution, FrontendApplication, TreeNode } from '@theia/core/lib/browser';
import { Range, EditorManager, EditorOpenerOptions } from '@theia/editor/lib/browser';
import { DisposableCollection, Disposable } from '@theia/core';
import { OutlineViewService } from '@theia/outline-view/lib/browser/outline-view-service';
import { OutlineSymbolInformationNode } from '@theia/outline-view/lib/browser/outline-view-widget';
import URI from '@theia/core/lib/common/uri';
import { MonacoEditor } from './monaco-editor';
import debounce = require('@theia/core/shared/lodash.debounce');
import * as monaco from '@theia/monaco-editor-core';
import { ILanguageFeaturesService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/languageFeatures';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
import { ITextModel } from '@theia/monaco-editor-core/esm/vs/editor/common/model';
import { DocumentSymbol } from '@theia/monaco-editor-core/esm/vs/editor/common/languages';
@injectable()
export class MonacoOutlineContribution implements FrontendApplicationContribution {
protected readonly toDisposeOnEditor = new DisposableCollection();
protected roots: MonacoOutlineSymbolInformationNode[] | undefined;
protected canUpdateOutline: boolean = true;
@inject(OutlineViewService) protected readonly outlineViewService: OutlineViewService;
@inject(EditorManager) protected readonly editorManager: EditorManager;
onStart(app: FrontendApplication): void {
// updateOutline and handleCurrentEditorChanged need to be called even when the outline view widget is closed
// in order to update breadcrumbs.
StandaloneServices.get(ILanguageFeaturesService).documentSymbolProvider.onDidChange(
debounce(() => this.updateOutline())
);
this.editorManager.onCurrentEditorChanged(
debounce(() => this.handleCurrentEditorChanged(), 50)
);
this.handleCurrentEditorChanged();
this.outlineViewService.onDidSelect(async node => {
if (MonacoOutlineSymbolInformationNode.is(node) && node.parent) {
const options: EditorOpenerOptions = {
mode: 'reveal',
selection: node.range
};
await this.selectInEditor(node, options);
}
});
this.outlineViewService.onDidOpen(async node => {
if (MonacoOutlineSymbolInformationNode.is(node)) {
const options: EditorOpenerOptions = {
selection: {
start: node.range.start
}
};
await this.selectInEditor(node, options);
}
});
}
protected async selectInEditor(node: MonacoOutlineSymbolInformationNode, options?: EditorOpenerOptions): Promise<void> {
// Avoid cyclic updates: Outline -> Editor -> Outline.
this.canUpdateOutline = false;
try {
await this.editorManager.open(node.uri, options);
} finally {
this.canUpdateOutline = true;
}
}
protected handleCurrentEditorChanged(): void {
this.toDisposeOnEditor.dispose();
this.toDisposeOnEditor.push(Disposable.create(() => this.roots = undefined));
const editor = this.editorManager.currentEditor;
if (editor) {
const model = MonacoEditor.get(editor)!.getControl().getModel();
if (model) {
this.toDisposeOnEditor.push(model.onDidChangeContent(() => {
this.roots = undefined; // Invalidate the previously resolved roots.
this.updateOutline();
}));
}
this.toDisposeOnEditor.push(editor.editor.onSelectionChanged(selection => this.updateOutline(selection)));
}
this.updateOutline();
}
protected tokenSource = new monaco.CancellationTokenSource();
protected async updateOutline(editorSelection?: Range): Promise<void> {
if (!this.canUpdateOutline) {
return;
}
this.tokenSource.cancel();
this.tokenSource = new monaco.CancellationTokenSource();
const token = this.tokenSource.token;
const editor = MonacoEditor.get(this.editorManager.currentEditor);
const model = editor && editor.getControl().getModel();
const roots = model && await this.createRoots(model, token, editorSelection);
if (token.isCancellationRequested) {
return;
}
this.outlineViewService.publish(roots || []);
}
protected async createRoots(
model: monaco.editor.ITextModel | ITextModel, token: monaco.CancellationToken, editorSelection?: Range
): Promise<MonacoOutlineSymbolInformationNode[]> {
model = model as ITextModel;
if (this.roots && this.roots.length > 0) {
// Reset the selection on the tree nodes, so that we can apply the new ones based on the `editorSelection`.
const resetSelection = (node: MonacoOutlineSymbolInformationNode) => {
node.selected = false;
node.children.forEach(resetSelection);
};
this.roots.forEach(resetSelection);
} else {
this.roots = [];
const providers = StandaloneServices.get(ILanguageFeaturesService).documentSymbolProvider.all(model);
if (token.isCancellationRequested) {
return [];
}
const uri = new URI(model.uri.toString());
for (const provider of providers) {
try {
const symbols = await provider.provideDocumentSymbols(model, token) ?? [];
if (token.isCancellationRequested) {
return [];
}
const nodes = this.createNodes(uri, symbols);
if (providers.length > 1 && provider.displayName) {
const providerRoot = this.createProviderRootNode(uri, provider.displayName, nodes);
this.roots.push(providerRoot);
} else {
this.roots.push(...nodes);
}
} catch {
/* collect symbols from other providers */
}
}
}
this.applySelection(this.roots, editorSelection);
return this.roots;
}
protected createProviderRootNode(uri: URI, displayName: string, children: MonacoOutlineSymbolInformationNode[]): MonacoOutlineSymbolInformationNode {
const node: MonacoOutlineSymbolInformationNode = {
uri,
id: displayName,
name: displayName,
iconClass: '',
range: this.asRange(new monaco.Range(1, 1, 1, 1)),
fullRange: this.asRange(new monaco.Range(1, 1, 1, 1)),
children,
parent: undefined,
selected: false,
expanded: true
};
return node;
}
protected createNodes(uri: URI, symbols: monaco.languages.DocumentSymbol[] | DocumentSymbol[]): MonacoOutlineSymbolInformationNode[] {
symbols = symbols as monaco.languages.DocumentSymbol[];
let rangeBased = false;
const ids = new Map();
const roots: MonacoOutlineSymbolInformationNode[] = [];
const nodesByName = symbols.sort(this.orderByPosition).reduce((result, symbol) => {
const node = this.createNode(uri, symbol, ids);
if (symbol.children) {
MonacoOutlineSymbolInformationNode.insert(roots, node);
} else {
rangeBased = rangeBased || symbol.range.startLineNumber !== symbol.range.endLineNumber;
const values = result.get(symbol.name) || [];
values.push({ symbol, node });
result.set(symbol.name, values);
}
return result;
}, new Map<string, MonacoOutlineContribution.NodeAndSymbol[]>());
for (const nodes of nodesByName.values()) {
for (const { node, symbol } of nodes) {
if (!symbol.containerName) {
MonacoOutlineSymbolInformationNode.insert(roots, node);
} else {
const possibleParents = nodesByName.get(symbol.containerName);
if (possibleParents) {
const parent = possibleParents.find(possibleParent => this.parentContains(symbol, possibleParent.symbol, rangeBased));
if (parent) {
node.parent = parent.node;
MonacoOutlineSymbolInformationNode.insert(parent.node.children, node);
}
}
}
}
}
if (!roots.length) {
const nodes = nodesByName.values().next().value;
if (nodes && !nodes[0].node.parent) {
return [nodes[0].node];
}
return [];
}
return roots;
}
/**
* Sets the selection on the sub-trees based on the optional editor selection.
* Select the narrowest node that is strictly contains the editor selection.
*/
protected applySelection(roots: MonacoOutlineSymbolInformationNode[], editorSelection?: Range): boolean {
if (editorSelection) {
for (const root of roots) {
if (this.parentContains(editorSelection, root.fullRange, true)) {
const { children } = root;
root.selected = !root.expanded || !this.applySelection(children, editorSelection);
return true;
}
}
}
return false;
}
/**
* Returns `true` if `candidate` is strictly contained inside `parent`
*
* If the argument is a `DocumentSymbol`, then `getFullRange` will be used to retrieve the range of the underlying symbol.
*/
protected parentContains(candidate: monaco.languages.DocumentSymbol | Range, parent: monaco.languages.DocumentSymbol | Range, rangeBased: boolean): boolean {
// TODO: move this code to the `monaco-languageclient`: https://github.com/eclipse-theia/theia/pull/2885#discussion_r217800446
const candidateRange = Range.is(candidate) ? candidate : this.getFullRange(candidate);
const parentRange = Range.is(parent) ? parent : this.getFullRange(parent);
const sameStartLine = candidateRange.start.line === parentRange.start.line;
const startColGreaterOrEqual = candidateRange.start.character >= parentRange.start.character;
const startLineGreater = candidateRange.start.line > parentRange.start.line;
const sameEndLine = candidateRange.end.line === parentRange.end.line;
const endColSmallerOrEqual = candidateRange.end.character <= parentRange.end.character;
const endLineSmaller = candidateRange.end.line < parentRange.end.line;
return (((sameStartLine && startColGreaterOrEqual || startLineGreater) &&
(sameEndLine && endColSmallerOrEqual || endLineSmaller)) || !rangeBased);
}
/**
* `monaco` to LSP `Range` converter. Converts the `1-based` location indices into `0-based` ones.
*/
protected asRange(range: monaco.IRange): Range {
const { startLineNumber, startColumn, endLineNumber, endColumn } = range;
return {
start: {
line: startLineNumber - 1,
character: startColumn - 1
},
end: {
line: endLineNumber - 1,
character: endColumn - 1
}
};
}
/**
* Returns with a range enclosing this symbol not including leading/trailing whitespace but everything else like comments.
* This information is typically used to determine if the clients cursor is inside the symbol to reveal in the symbol in the UI.
* This allows to obtain the range including the associated comments.
*
* See: [`DocumentSymbol#range`](https://microsoft.github.io/language-server-protocol/specification#textDocument_documentSymbol) for more details.
*/
protected getFullRange(documentSymbol: monaco.languages.DocumentSymbol): Range {
return this.asRange(documentSymbol.range);
}
/**
* The range that should be selected and revealed when this symbol is being picked, e.g the name of a function. Must be contained by the `getSelectionRange`.
*
* See: [`DocumentSymbol#selectionRange`](https://microsoft.github.io/language-server-protocol/specification#textDocument_documentSymbol) for more details.
*/
protected getNameRange(documentSymbol: monaco.languages.DocumentSymbol): Range {
return this.asRange(documentSymbol.selectionRange);
}
protected createNode(
uri: URI, symbol: monaco.languages.DocumentSymbol, ids: Map<string, number>, parent?: MonacoOutlineSymbolInformationNode
): MonacoOutlineSymbolInformationNode {
const id = this.createId(symbol.name, ids);
const children: MonacoOutlineSymbolInformationNode[] = [];
const node: MonacoOutlineSymbolInformationNode = {
children,
id,
iconClass: monaco.languages.SymbolKind[symbol.kind].toString().toLowerCase(),
name: this.getName(symbol),
detail: this.getDetail(symbol),
parent,
uri,
range: this.getNameRange(symbol),
fullRange: this.getFullRange(symbol),
selected: false,
expanded: this.shouldExpand(symbol)
};
if (symbol.children) {
for (const child of symbol.children) {
MonacoOutlineSymbolInformationNode.insert(children, this.createNode(uri, child, ids, node));
}
}
return node;
}
protected getName(symbol: monaco.languages.DocumentSymbol): string {
return symbol.name;
}
protected getDetail(symbol: monaco.languages.DocumentSymbol): string {
return symbol.detail;
}
protected createId(name: string, ids: Map<string, number>): string {
const counter = ids.get(name);
const index = typeof counter === 'number' ? counter + 1 : 0;
ids.set(name, index);
return name + '_' + index;
}
protected shouldExpand(symbol: monaco.languages.DocumentSymbol): boolean {
return [
monaco.languages.SymbolKind.Class,
monaco.languages.SymbolKind.Enum, monaco.languages.SymbolKind.File,
monaco.languages.SymbolKind.Interface, monaco.languages.SymbolKind.Module,
monaco.languages.SymbolKind.Namespace, monaco.languages.SymbolKind.Object,
monaco.languages.SymbolKind.Package, monaco.languages.SymbolKind.Struct
].indexOf(symbol.kind) !== -1;
}
protected orderByPosition(symbol: monaco.languages.DocumentSymbol, symbol2: monaco.languages.DocumentSymbol): number {
const startLineComparison = symbol.range.startLineNumber - symbol2.range.startLineNumber;
if (startLineComparison !== 0) {
return startLineComparison;
}
const startOffsetComparison = symbol.range.startColumn - symbol2.range.startColumn;
if (startOffsetComparison !== 0) {
return startOffsetComparison;
}
const endLineComparison = symbol.range.endLineNumber - symbol2.range.endLineNumber;
if (endLineComparison !== 0) {
return endLineComparison;
}
return symbol.range.endColumn - symbol2.range.endColumn;
}
}
export namespace MonacoOutlineContribution {
export interface NodeAndSymbol {
node: MonacoOutlineSymbolInformationNode;
symbol: monaco.languages.DocumentSymbol
}
}
export interface MonacoOutlineSymbolInformationNode extends OutlineSymbolInformationNode {
uri: URI;
range: Range;
fullRange: Range;
detail?: string;
parent: MonacoOutlineSymbolInformationNode | undefined;
children: MonacoOutlineSymbolInformationNode[];
}
export namespace MonacoOutlineSymbolInformationNode {
export function is(node: TreeNode): node is MonacoOutlineSymbolInformationNode {
return OutlineSymbolInformationNode.is(node) && 'uri' in node && 'range' in node;
}
export function insert(nodes: MonacoOutlineSymbolInformationNode[], node: MonacoOutlineSymbolInformationNode): void {
const index = nodes.findIndex(current => compare(node, current) < 0);
if (index === -1) {
nodes.push(node);
} else {
nodes.splice(index, 0, node);
}
}
export function compare(node: MonacoOutlineSymbolInformationNode, node2: MonacoOutlineSymbolInformationNode): number {
const startLineComparison = node.range.start.line - node2.range.start.line;
if (startLineComparison !== 0) {
return startLineComparison;
}
const startColumnComparison = node.range.start.character - node2.range.start.character;
if (startColumnComparison !== 0) {
return startColumnComparison;
}
const endLineComparison = node2.range.end.line - node.range.end.line;
if (endLineComparison !== 0) {
return endLineComparison;
}
return node2.range.end.character - node.range.end.character;
}
}

View File

@@ -0,0 +1,66 @@
// *****************************************************************************
// Copyright (C) 2018 RedHat and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { Event, Emitter } from '@theia/core/lib/common/event';
import { Tree } from '@theia/core/lib/browser/tree/tree';
import { DepthFirstTreeIterator } from '@theia/core/lib/browser/tree/tree-iterator';
import { TreeDecorator, TreeDecoration } from '@theia/core/lib/browser/tree/tree-decorator';
import { MonacoOutlineSymbolInformationNode } from './monaco-outline-contribution';
@injectable()
export class MonacoOutlineDecorator implements TreeDecorator {
readonly id = 'theia-monaco-outline-decorator';
protected readonly emitter = new Emitter<(tree: Tree) => Map<string, TreeDecoration.Data>>();
async decorations(tree: Tree): Promise<Map<string, TreeDecoration.Data>> {
return this.collectDecorations(tree);
}
get onDidChangeDecorations(): Event<(tree: Tree) => Map<string, TreeDecoration.Data>> {
return this.emitter.event;
}
protected collectDecorations(tree: Tree): Map<string, TreeDecoration.Data> {
const result = new Map();
if (tree.root === undefined) {
return result;
}
for (const treeNode of new DepthFirstTreeIterator(tree.root)) {
if (MonacoOutlineSymbolInformationNode.is(treeNode) && treeNode.detail) {
result.set(treeNode.id, this.toDecoration(treeNode));
}
}
return result;
}
protected toDecoration(node: MonacoOutlineSymbolInformationNode): TreeDecoration.Data {
const captionSuffixes: TreeDecoration.CaptionAffix[] = [{
data: (node.detail || ''),
fontData: {
color: 'var(--theia-descriptionForeground)',
}
}];
return {
captionSuffixes
};
}
}

View File

@@ -0,0 +1,112 @@
// *****************************************************************************
// Copyright (C) 2021 Red Hat and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { KeybindingRegistry, QuickPickItem, QuickPickSeparator } from '@theia/core/lib/browser';
import { QuickAccessProviderDescriptor, QuickAccessRegistry } from '@theia/core/lib/browser/quick-input/quick-access';
import { CancellationToken, Disposable } from '@theia/core/lib/common';
import { inject, injectable } from '@theia/core/shared/inversify';
import { MonacoQuickPickItem } from './monaco-quick-input-service';
import {
IPickerQuickAccessProviderOptions, PickerQuickAccessProvider, Picks, Pick, IPickerQuickAccessItem
} from '@theia/monaco-editor-core/esm/vs/platform/quickinput/browser/pickerQuickAccess';
import {
Extensions,
IQuickAccessProvider,
IQuickAccessProviderDescriptor,
IQuickAccessProviderHelp,
IQuickAccessRegistry,
QuickAccessRegistry as VSCodeQuickAccessRegistry,
} from '@theia/monaco-editor-core/esm/vs/platform/quickinput/common/quickAccess';
import { IQuickPickItem, IQuickPickItemWithResource } from '@theia/monaco-editor-core/esm/vs/platform/quickinput/common/quickInput';
import { Registry } from '@theia/monaco-editor-core/esm/vs/platform/registry/common/platform';
interface IAnythingQuickPickItem extends IPickerQuickAccessItem, IQuickPickItemWithResource { }
abstract class MonacoPickerAccessProvider extends PickerQuickAccessProvider<IQuickPickItem> {
constructor(prefix: string, options?: IPickerQuickAccessProviderOptions<IQuickPickItem>) {
super(prefix, options);
}
abstract getDescriptor(): QuickAccessProviderDescriptor;
}
class TheiaQuickAccessDescriptor implements IQuickAccessProviderDescriptor {
constructor(
public readonly theiaDescriptor: QuickAccessProviderDescriptor,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
readonly ctor: { new(...services: any /* TS BrandedService but no clue how to type this properly */[]): IQuickAccessProvider },
readonly prefix: string,
readonly helpEntries: IQuickAccessProviderHelp[],
readonly placeholder?: string) { }
}
@injectable()
export class MonacoQuickAccessRegistry implements QuickAccessRegistry {
@inject(KeybindingRegistry)
protected readonly keybindingRegistry: KeybindingRegistry;
private get monacoRegistry(): IQuickAccessRegistry {
return Registry.as<IQuickAccessRegistry>(Extensions.Quickaccess);
}
registerQuickAccessProvider(descriptor: QuickAccessProviderDescriptor): Disposable {
const toMonacoPick = (item: QuickPickItem): Pick<IAnythingQuickPickItem> => {
if (QuickPickSeparator.is(item)) {
return item;
} else {
return new MonacoQuickPickItem(item, this.keybindingRegistry);
}
};
const inner =
class extends MonacoPickerAccessProvider {
getDescriptor(): QuickAccessProviderDescriptor {
return descriptor;
}
constructor() {
super(descriptor.prefix);
}
protected override async _getPicks(filter: string, disposables: unknown, token: CancellationToken): Promise<Picks<IQuickPickItem>> {
const result = await Promise.resolve(descriptor.getInstance().getPicks(filter, token));
return result.map(toMonacoPick);
}
};
return this.monacoRegistry.registerQuickAccessProvider(new TheiaQuickAccessDescriptor(
descriptor,
inner,
descriptor.prefix,
descriptor.helpEntries,
descriptor.placeholder
));
}
getQuickAccessProviders(): QuickAccessProviderDescriptor[] {
return this.monacoRegistry.getQuickAccessProviders()
.filter(provider => provider instanceof TheiaQuickAccessDescriptor)
.map(provider => (provider as TheiaQuickAccessDescriptor).theiaDescriptor);
}
getQuickAccessProvider(prefix: string): QuickAccessProviderDescriptor | undefined {
const monacoDescriptor = this.monacoRegistry.getQuickAccessProvider(prefix);
return monacoDescriptor ? (monacoDescriptor as TheiaQuickAccessDescriptor).theiaDescriptor : undefined;
}
clear(): void {
if (this.monacoRegistry instanceof VSCodeQuickAccessRegistry) {
this.monacoRegistry.clear();
}
}
}

View File

@@ -0,0 +1,739 @@
// *****************************************************************************
// Copyright (C) 2021 SAP SE or an SAP affiliate company and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import {
ApplicationShell,
InputBox, InputOptions, KeybindingRegistry, PickOptions,
QuickInputButton, QuickInputHideReason, QuickInputService, QuickPick, QuickPickItem,
QuickPickItemButtonEvent, QuickPickItemHighlights, QuickPickOptions, QuickPickSeparator
} from '@theia/core/lib/browser';
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import {
IInputBox, IInputOptions, IKeyMods, IPickOptions, IQuickInput, IQuickInputButton,
IQuickInputService, IQuickNavigateConfiguration, IQuickPick, IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator, IQuickWidget, QuickPickInput
} from '@theia/monaco-editor-core/esm/vs/platform/quickinput/common/quickInput';
import { IQuickInputOptions, IQuickInputStyles } from '@theia/monaco-editor-core/esm/vs/platform/quickinput/browser/quickInput';
import { QuickInputController } from '@theia/monaco-editor-core/esm/vs/platform/quickinput/browser/quickInputController';
import { MonacoResolvedKeybinding } from './monaco-resolved-keybinding';
import { IQuickAccessController } from '@theia/monaco-editor-core/esm/vs/platform/quickinput/common/quickAccess';
import { QuickAccessController } from '@theia/monaco-editor-core/esm/vs/platform/quickinput/browser/quickAccess';
import { IContextKey, IContextKeyService, IScopedContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/common/contextkey';
import * as monaco from '@theia/monaco-editor-core';
import { ResolvedKeybinding } from '@theia/monaco-editor-core/esm/vs/base/common/keybindings';
import { IInstantiationService } from '@theia/monaco-editor-core/esm/vs/platform/instantiation/common/instantiation';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
import { IMatch } from '@theia/monaco-editor-core/esm/vs/base/common/filters';
import { CancellationToken, Event } from '@theia/core';
import { MonacoColorRegistry } from './monaco-color-registry';
import { ThemeService } from '@theia/core/lib/browser/theming';
import { IStandaloneThemeService } from '@theia/monaco-editor-core/esm/vs/editor/standalone/common/standaloneTheme';
import { ILayoutService } from '@theia/monaco-editor-core/esm/vs/platform/layout/browser/layoutService';
import { IHoverDelegate, IHoverDelegateOptions } from '@theia/monaco-editor-core/esm/vs/base/browser/ui/hover/hoverDelegate';
import { IHoverWidget } from '@theia/monaco-editor-core/esm/vs/base/browser/ui/hover/hover';
// Copied from @vscode/src/vs/base/parts/quickInput/browser/quickInputList.ts
export interface IListElement {
readonly index: number;
readonly item: IQuickPickItem;
readonly saneLabel: string;
readonly saneAriaLabel: string;
readonly saneDescription?: string;
readonly saneDetail?: string;
readonly labelHighlights?: IMatch[];
readonly descriptionHighlights?: IMatch[];
readonly detailHighlights?: IMatch[];
readonly checked: boolean;
readonly separator?: IQuickPickSeparator;
readonly fireButtonTriggered: (event: IQuickPickItemButtonEvent<IQuickPickItem>) => void;
}
class HoverDelegate implements IHoverDelegate {
showHover(options: IHoverDelegateOptions, focus?: boolean | undefined): IHoverWidget | undefined {
return undefined;
}
onDidHideHover?: (() => void) | undefined;
delay: number;
placement?: 'mouse' | 'element' | undefined;
showNativeHover?: boolean | undefined;
}
@injectable()
export class MonacoQuickInputImplementation implements IQuickInputService {
get currentQuickInput(): IQuickInput | undefined {
return this.controller.currentQuickInput;
}
declare readonly _serviceBrand: undefined;
controller: QuickInputController;
quickAccess: IQuickAccessController;
@inject(ApplicationShell)
protected readonly shell: ApplicationShell;
@inject(MonacoColorRegistry)
protected readonly colorRegistry: MonacoColorRegistry;
@inject(ThemeService)
protected readonly themeService: ThemeService;
protected container: HTMLElement;
protected inQuickOpen: IContextKey<boolean>;
/**
* Help keybinding service prioritize keybindings when this element is focused.
*/
protected scopedInQuickOpen: IContextKey<boolean>;
/**
* Help keybinding service prioritize keybindings when this element is focused.
*/
protected scopedContextKeyService: IScopedContextKeyService;
get backButton(): IQuickInputButton { return this.controller.backButton; }
get onShow(): monaco.IEvent<void> { return this.controller.onShow; }
get onHide(): monaco.IEvent<void> { return this.controller.onHide; }
@postConstruct()
protected init(): void {
this.initContainer();
this.initController();
this.quickAccess = new QuickAccessController(this, StandaloneServices.get(IInstantiationService));
const contextService = StandaloneServices.get(IContextKeyService);
this.inQuickOpen = contextService.createKey<boolean>('inQuickOpen', false);
this.scopedContextKeyService = contextService.createScoped(this.container);
this.scopedInQuickOpen = this.scopedContextKeyService.createKey<boolean>('inQuickOpen', false);
this.controller.onShow(() => {
this.container.style.top = this.shell.mainPanel.node.getBoundingClientRect().top + 'px';
this.inQuickOpen.set(true);
this.scopedInQuickOpen.set(true);
});
this.controller.onHide(() => {
this.inQuickOpen.set(false);
this.scopedInQuickOpen.set(false);
});
this.themeService.initialized.then(() => this.controller.applyStyles(this.computeStyles()));
// Hook into the theming service of Monaco to ensure that the updates are ready.
StandaloneServices.get(IStandaloneThemeService).onDidColorThemeChange(() => this.controller.applyStyles(this.computeStyles()));
window.addEventListener('resize', () => this.updateLayout());
}
setContextKey(key: string | undefined): void {
if (key) {
StandaloneServices.get(IContextKeyService).createKey<string>(key, undefined);
}
}
createQuickWidget(): IQuickWidget {
return this.controller.createQuickWidget();
}
createQuickPick<T extends IQuickPickItem>(options: { useSeparators: true; }): IQuickPick<T, { useSeparators: true; }>;
createQuickPick<T extends IQuickPickItem>(options: { useSeparators: false; }): IQuickPick<T, { useSeparators: false; }>;
createQuickPick<T extends IQuickPickItem>(options: { useSeparators: boolean }): IQuickPick<T, { useSeparators: true; }> | IQuickPick<T, { useSeparators: false; }> {
return this.controller.createQuickPick({
useSeparators: options.useSeparators
});
}
createInputBox(): IInputBox {
return this.controller.createInputBox();
}
open(filter: string): void {
this.quickAccess.show(filter);
}
input(options?: IInputOptions, token?: monaco.CancellationToken): Promise<string | undefined> {
return this.controller.input(options, token);
}
pick<T extends IQuickPickItem, O extends IPickOptions<T>>(
picks: Promise<QuickPickInput<T>[]> | QuickPickInput<T>[], options?: O, token?: monaco.CancellationToken
): Promise<(O extends { canPickMany: true; } ? T[] : T) | undefined> {
return this.controller.pick(picks, options, token);
}
hide(): void {
this.controller.hide();
}
focus(): void {
this.controller.focus();
}
toggle(): void {
this.controller.toggle();
}
applyStyles(styles: IQuickInputStyles): void {
this.controller.applyStyles(styles);
}
layout(dimension: monaco.editor.IDimension, titleBarOffset: number): void {
this.controller.layout(dimension, titleBarOffset);
}
navigate(next: boolean, quickNavigate?: IQuickNavigateConfiguration): void {
this.controller.navigate(next, quickNavigate);
}
dispose(): void {
this.scopedContextKeyService.dispose();
this.controller.dispose();
}
async cancel(): Promise<void> {
this.controller.cancel();
}
async back(): Promise<void> {
this.controller.back();
}
async accept(keyMods?: IKeyMods): Promise<void> {
this.controller.accept(keyMods);
}
private initContainer(): void {
const container = this.container = document.createElement('div');
container.id = 'quick-input-container';
document.body.appendChild(this.container);
}
private initController(): void {
const contextKeyService = StandaloneServices.get(IContextKeyService);
const instantiationService = StandaloneServices.get(IInstantiationService);
const layoutService = StandaloneServices.get(ILayoutService);
const options: IQuickInputOptions = {
idPrefix: 'quickInput_',
container: this.container,
styles: this.computeStyles(),
ignoreFocusOut: () => false,
backKeybindingLabel: () => undefined,
setContextKey: (id?: string) => this.setContextKey(id),
returnFocus: () => this.container.focus(),
hoverDelegate: new HoverDelegate(),
linkOpenerDelegate: () => {
// @monaco-uplift: not sure what to do here
}
};
this.controller = new QuickInputController(options, layoutService, instantiationService, contextKeyService);
this.updateLayout();
}
private updateLayout(): void {
// Initialize the layout using screen dimensions as monaco computes the actual sizing.
// https://github.com/microsoft/vscode/blob/6261075646f055b99068d3688932416f2346dd3b/src/vs/base/parts/quickinput/browser/quickInput.ts#L1799
this.controller.layout(this.getClientDimension(), 0);
}
private getClientDimension(): monaco.editor.IDimension {
return { width: window.innerWidth, height: window.innerHeight };
}
// @monaco-uplift
// Keep the styles up to date with https://github.com/microsoft/vscode/blob/7888ff3a6b104e9e2e3d0f7890ca92dd0828215f/src/vs/platform/quickinput/browser/quickInput.ts#L171.
private computeStyles(): IQuickInputStyles {
return {
toggle: {
inputActiveOptionBorder: this.colorRegistry.toCssVariableName('inputOption.activeBorder'),
inputActiveOptionForeground: this.colorRegistry.toCssVariableName('inputOption.activeForeground'),
inputActiveOptionBackground: this.colorRegistry.toCssVariableName('inputOption.activeBackground')
},
pickerGroup: {
pickerGroupBorder: this.colorRegistry.toCssVariableName('pickerGroup.Border'),
pickerGroupForeground: this.colorRegistry.toCssVariableName('pickerGroupForeground')
},
widget: {
quickInputBackground: this.colorRegistry.toCssVariableName('quickInput.background'),
quickInputForeground: this.colorRegistry.toCssVariableName('quickInput.foreground'),
quickInputTitleBackground: this.colorRegistry.toCssVariableName('quickInputTitle.background'),
widgetBorder: this.colorRegistry.toCssVariableName('widget.border'),
widgetShadow: this.colorRegistry.toCssVariableName('widget.shadow')
},
list: {
listBackground: this.colorRegistry.toCssVariableName('quickInput.background'),
listInactiveFocusForeground: this.colorRegistry.toCssVariableName('quickInputList.focusForeground'),
listInactiveSelectionIconForeground: this.colorRegistry.toCssVariableName('quickInputList.focusIconForeground'),
listInactiveFocusBackground: this.colorRegistry.toCssVariableName('quickInputList.focusBackground'),
listFocusOutline: this.colorRegistry.toCssVariableName('activeContrastBorder'),
listInactiveFocusOutline: this.colorRegistry.toCssVariableName('activeContrastBorder'),
listFocusBackground: this.colorRegistry.toCssVariableName('list.focusBackground'),
listFocusForeground: this.colorRegistry.toCssVariableName('list.focusForeground'),
listActiveSelectionBackground: this.colorRegistry.toCssVariableName('list.activeSelectionBackground'),
listActiveSelectionForeground: this.colorRegistry.toCssVariableName('list.ActiveSelectionForeground'),
listActiveSelectionIconForeground: this.colorRegistry.toCssVariableName('list.ActiveSelectionIconForeground'),
listFocusAndSelectionOutline: this.colorRegistry.toCssVariableName('list.FocusAndSelectionOutline'),
listFocusAndSelectionBackground: this.colorRegistry.toCssVariableName('list.ActiveSelectionBackground'),
listFocusAndSelectionForeground: this.colorRegistry.toCssVariableName('list.ActiveSelectionForeground'),
listInactiveSelectionBackground: this.colorRegistry.toCssVariableName('list.InactiveSelectionBackground'),
listInactiveSelectionForeground: this.colorRegistry.toCssVariableName('list.InactiveSelectionForeground'),
listHoverBackground: this.colorRegistry.toCssVariableName('list.HoverBackground'),
listHoverForeground: this.colorRegistry.toCssVariableName('list.HoverForeground'),
listDropOverBackground: this.colorRegistry.toCssVariableName('list.DropOverBackground'),
listDropBetweenBackground: this.colorRegistry.toCssVariableName('list.DropBetweenBackground'),
listSelectionOutline: this.colorRegistry.toCssVariableName('activeContrastBorder'),
listHoverOutline: this.colorRegistry.toCssVariableName('activeContrastBorder'),
treeIndentGuidesStroke: this.colorRegistry.toCssVariableName('tree.indentGuidesStroke'),
treeInactiveIndentGuidesStroke: this.colorRegistry.toCssVariableName('tree.inactiveIndentGuidesStroke'),
treeStickyScrollBackground: this.colorRegistry.toCssVariableName('tree.StickyScrollBackground'),
treeStickyScrollBorder: this.colorRegistry.toCssVariableName('tree.tickyScrollBorde'),
treeStickyScrollShadow: this.colorRegistry.toCssVariableName('tree.StickyScrollShadow'),
tableColumnsBorder: this.colorRegistry.toCssVariableName('tree.tableColumnsBorder'),
tableOddRowsBackgroundColor: this.colorRegistry.toCssVariableName('tree.tableOddRowsBackground'),
},
inputBox: {
inputForeground: this.colorRegistry.toCssVariableName('inputForeground'),
inputBackground: this.colorRegistry.toCssVariableName('inputBackground'),
inputBorder: this.colorRegistry.toCssVariableName('inputBorder'),
inputValidationInfoBackground: this.colorRegistry.toCssVariableName('inputValidation.infoBackground'),
inputValidationInfoForeground: this.colorRegistry.toCssVariableName('inputValidation.infoForeground'),
inputValidationInfoBorder: this.colorRegistry.toCssVariableName('inputValidation.infoBorder'),
inputValidationWarningBackground: this.colorRegistry.toCssVariableName('inputValidation.warningBackground'),
inputValidationWarningForeground: this.colorRegistry.toCssVariableName('inputValidation.warningForeground'),
inputValidationWarningBorder: this.colorRegistry.toCssVariableName('inputValidation.warningBorder'),
inputValidationErrorBackground: this.colorRegistry.toCssVariableName('inputValidation.errorBackground'),
inputValidationErrorForeground: this.colorRegistry.toCssVariableName('inputValidation.errorForeground'),
inputValidationErrorBorder: this.colorRegistry.toCssVariableName('inputValidation.errorBorder'),
},
countBadge: {
badgeBackground: this.colorRegistry.toCssVariableName('badge.background'),
badgeForeground: this.colorRegistry.toCssVariableName('badge.foreground'),
badgeBorder: this.colorRegistry.toCssVariableName('contrastBorder')
},
button: {
buttonForeground: this.colorRegistry.toCssVariableName('button.foreground'),
buttonBackground: this.colorRegistry.toCssVariableName('button.background'),
buttonHoverBackground: this.colorRegistry.toCssVariableName('button.hoverBackground'),
buttonBorder: this.colorRegistry.toCssVariableName('contrastBorder'),
buttonSeparator: this.colorRegistry.toCssVariableName('button.Separator'),
buttonSecondaryForeground: this.colorRegistry.toCssVariableName('button.secondaryForeground'),
buttonSecondaryBackground: this.colorRegistry.toCssVariableName('button.secondaryBackground'),
buttonSecondaryHoverBackground: this.colorRegistry.toCssVariableName('button.secondaryHoverBackground'),
},
progressBar: {
progressBarBackground: this.colorRegistry.toCssVariableName('progressBar.background')
},
keybindingLabel: {
keybindingLabelBackground: this.colorRegistry.toCssVariableName('keybindingLabel.background'),
keybindingLabelForeground: this.colorRegistry.toCssVariableName('keybindingLabel.foreground'),
keybindingLabelBorder: this.colorRegistry.toCssVariableName('keybindingLabel.border'),
keybindingLabelBottomBorder: this.colorRegistry.toCssVariableName('keybindingLabel.bottomBorder'),
keybindingLabelShadow: this.colorRegistry.toCssVariableName('widget.shadow')
},
};
}
}
@injectable()
export class MonacoQuickInputService implements QuickInputService {
@inject(MonacoQuickInputImplementation)
private monacoService: MonacoQuickInputImplementation;
@inject(KeybindingRegistry)
protected readonly keybindingRegistry: KeybindingRegistry;
get backButton(): QuickInputButton {
// need to cast because of vscode issue https://github.com/microsoft/vscode/issues/190584
return this.monacoService.backButton as QuickInputButton;
}
get onShow(): Event<void> { return this.monacoService.onShow; }
get onHide(): Event<void> { return this.monacoService.onHide; }
open(filter: string): void {
this.monacoService.open(filter);
}
createInputBox(): InputBox {
// need to cast because of vscode issue https://github.com/microsoft/vscode/issues/190584
return this.monacoService.createInputBox() as InputBox;
}
input(options?: InputOptions, token?: monaco.CancellationToken): Promise<string | undefined> {
let inputOptions: IInputOptions | undefined;
if (options) {
const { validateInput, ...props } = options;
inputOptions = { ...props };
if (validateInput) {
inputOptions.validateInput = async input => validateInput(input);
}
}
return this.monacoService.input(inputOptions, token);
}
async pick<T extends QuickPickItem, O extends PickOptions<T> = PickOptions<T>>(
picks: Promise<QuickPickInput<T>[]> | QuickPickInput<T>[], options?: O, token?: CancellationToken
): Promise<T[] | T | undefined> {
return this.monacoService.pick(picks, options, token);
}
showQuickPick<T extends QuickPickItem>(items: Array<T | QuickPickSeparator>, options?: QuickPickOptions<T>): Promise<T | undefined> {
return new Promise<T | undefined>((resolve, reject) => {
const wrapped = this.createQuickPick<T>();
wrapped.items = items;
if (options) {
wrapped.canSelectMany = !!options.canSelectMany;
wrapped.contextKey = options.contextKey;
wrapped.description = options.description;
wrapped.enabled = options.enabled ?? true;
wrapped.ignoreFocusOut = !!options.ignoreFocusOut;
wrapped.matchOnDescription = options.matchOnDescription ?? true;
wrapped.matchOnDetail = options.matchOnDetail ?? true;
wrapped.keepScrollPosition = options.keepScrollPosition ?? false;
wrapped.placeholder = options.placeholder;
wrapped.step = options.step;
wrapped.title = options.title;
wrapped.totalSteps = options.totalSteps;
if (options.activeItem) {
wrapped.activeItems = [options.activeItem];
}
wrapped.onDidChangeValue((filter: string) => {
if (options.onDidChangeValue) {
options.onDidChangeValue(wrapped, filter);
}
});
wrapped.onDidChangeActive((activeItems: Array<T>) => {
if (options.onDidChangeActive) {
options.onDidChangeActive(wrapped, activeItems);
}
});
wrapped.onDidTriggerButton((button: IQuickInputButton) => {
if (options.onDidTriggerButton) {
// need to cast because of vscode issue https://github.com/microsoft/vscode/issues/190584
options.onDidTriggerButton(button as QuickInputButton);
}
});
wrapped.onDidTriggerItemButton((event: QuickPickItemButtonEvent<T>) => {
if (options.onDidTriggerItemButton) {
// https://github.com/theia-ide/vscode/blob/standalone/0.23.x/src/vs/base/parts/quickinput/browser/quickInput.ts#L1387
options.onDidTriggerItemButton(
{
...event,
removeItem: () => {
wrapped.items = wrapped.items.filter(item => item !== event.item);
wrapped.activeItems = wrapped.activeItems.filter(item => item !== event.item);
}
});
}
});
wrapped.onDidChangeSelection((selectedItems: Array<T>) => {
if (options.onDidChangeSelection) {
options.onDidChangeSelection(wrapped, selectedItems);
}
});
}
wrapped.onDidAccept(() => {
if (options?.onDidAccept) {
options.onDidAccept();
}
wrapped.hide();
resolve(wrapped.selectedItems[0]);
});
wrapped.onDidHide(() => {
if (options?.onDidHide) {
options?.onDidHide();
};
wrapped.dispose();
setTimeout(() => resolve(undefined));
});
wrapped.show();
}).then(item => {
if (item?.execute) {
item.execute();
}
return item;
});
}
createQuickPick<T extends QuickPickItem>(): QuickPick<T> {
const quickPick = this.monacoService.createQuickPick<MonacoQuickPickItem<T>>({ useSeparators: true });
return this.wrapQuickPick(quickPick);
}
wrapQuickPick<T extends QuickPickItem>(wrapped: IQuickPick<MonacoQuickPickItem<T>, { useSeparators: true }>): QuickPick<T> {
return new MonacoQuickPick(wrapped, this.keybindingRegistry);
}
protected convertItems<T extends QuickPickItem>(item: T): MonacoQuickPickItem<T> {
return new MonacoQuickPickItem(item, this.keybindingRegistry);
}
hide(): void {
return this.monacoService.hide();
}
}
class MonacoQuickInput {
constructor(protected readonly wrapped: IQuickInput) {
}
get onDidHide(): Event<{ reason: QuickInputHideReason }> { return this.wrapped.onDidHide; }
get onDispose(): Event<void> { return this.wrapped.onDispose; }
get title(): string | undefined {
return this.wrapped.title;
}
set title(v: string | undefined) {
this.wrapped.title = v;
}
get description(): string | undefined {
return this.wrapped.description;
}
set description(v: string | undefined) {
this.wrapped.description = v;
}
get step(): number | undefined {
return this.wrapped.step;
}
set step(v: number | undefined) {
this.wrapped.step = v;
}
get enabled(): boolean {
return this.wrapped.enabled;
}
set enabled(v: boolean) {
this.wrapped.enabled = v;
}
get totalSteps(): number | undefined {
return this.wrapped.totalSteps;
}
set totalSteps(v: number | undefined) {
this.wrapped.totalSteps = v;
}
get contextKey(): string | undefined {
return this.wrapped.contextKey;
}
set contextKey(v: string | undefined) {
this.wrapped.contextKey = v;
}
get busy(): boolean {
return this.wrapped.busy;
}
set busy(v: boolean) {
this.wrapped.busy = v;
}
get ignoreFocusOut(): boolean {
return this.wrapped.ignoreFocusOut;
}
set ignoreFocusOut(v: boolean) {
this.wrapped.ignoreFocusOut = v;
}
show(): void {
this.wrapped.show();
}
hide(): void {
this.wrapped.hide();
}
dispose(): void {
this.wrapped.dispose();
}
}
class MonacoQuickPick<T extends QuickPickItem> extends MonacoQuickInput implements QuickPick<T> {
constructor(protected override readonly wrapped: IQuickPick<MonacoQuickPickItem<T>, { useSeparators: true }>, protected readonly keybindingRegistry: KeybindingRegistry) {
super(wrapped);
}
get value(): string {
return this.wrapped.value;
};
set value(v: string) {
this.wrapped.value = v;
}
get placeholder(): string | undefined {
return this.wrapped.placeholder;
}
set placeholder(v: string | undefined) {
this.wrapped.placeholder = v;
}
get canSelectMany(): boolean {
return this.wrapped.canSelectMany;
}
set canSelectMany(v: boolean) {
this.wrapped.canSelectMany = v;
}
get matchOnDescription(): boolean {
return this.wrapped.matchOnDescription;
}
set matchOnDescription(v: boolean) {
this.wrapped.matchOnDescription = v;
}
get matchOnDetail(): boolean {
return this.wrapped.matchOnDetail;
}
set matchOnDetail(v: boolean) {
this.wrapped.matchOnDetail = v;
}
get keepScrollPosition(): boolean {
return this.wrapped.keepScrollPosition;
}
set keepScrollPosition(v: boolean) {
this.wrapped.keepScrollPosition = v;
}
get items(): readonly (T | QuickPickSeparator)[] {
// need to cast because of vscode issue https://github.com/microsoft/vscode/issues/190584
return this.wrapped.items.map(item => {
if (item instanceof MonacoQuickPickItem) {
return item.item;
} else {
return item;
}
});
}
get buttons(): ReadonlyArray<QuickInputButton> {
return this.wrapped.buttons as QuickInputButton[];
}
set buttons(buttons: ReadonlyArray<QuickInputButton>) {
this.wrapped.buttons = buttons;
}
set items(itemList: readonly (T | QuickPickSeparator)[]) {
// We need to store and apply the currently selected active items.
// Since monaco compares these items by reference equality, creating new wrapped items will unmark any active items.
// Assigning the `activeItems` again will restore all active items even after the items array has changed.
// See also the `findMonacoItemReferences` method.
const active = this.activeItems;
this.wrapped.items = itemList.map(item => QuickPickSeparator.is(item) ? item : new MonacoQuickPickItem<T>(item, this.keybindingRegistry));
if (active.length !== 0) {
this.activeItems = active; // If this is done with an empty activeItems array, then it will undo first item focus on quick menus.
}
}
set activeItems(itemList: readonly T[]) {
this.wrapped.activeItems = this.findMonacoItemReferences(this.wrapped.items, itemList);
}
get activeItems(): readonly (T)[] {
return this.wrapped.activeItems.map(item => item.item);
}
set selectedItems(itemList: readonly T[]) {
this.wrapped.selectedItems = this.findMonacoItemReferences(this.wrapped.items, itemList);
}
get selectedItems(): readonly (T)[] {
return this.wrapped.selectedItems.map(item => item.item);
}
readonly onDidAccept: Event<{ inBackground: boolean }> = this.wrapped.onDidAccept;
readonly onDidChangeValue: Event<string> = this.wrapped.onDidChangeValue;
// need to cast because of vscode issue https://github.com/microsoft/vscode/issues/190584
readonly onDidTriggerButton: Event<QuickInputButton> = this.wrapped.onDidTriggerButton as Event<QuickInputButton>;
readonly onDidTriggerItemButton: Event<QuickPickItemButtonEvent<T>> =
Event.map(this.wrapped.onDidTriggerItemButton, (evt: IQuickPickItemButtonEvent<MonacoQuickPickItem<T>>) => ({
item: evt.item.item,
button: evt.button
})) as Event<QuickPickItemButtonEvent<T>>;
readonly onDidChangeActive: Event<T[]> = Event.map(
this.wrapped.onDidChangeActive,
(items: MonacoQuickPickItem<T>[]) => items.map(item => item.item));
readonly onDidChangeSelection: Event<T[]> = Event.map(
this.wrapped.onDidChangeSelection, (items: MonacoQuickPickItem<T>[]) => items.map(item => item.item));
/**
* Monaco doesn't check for deep equality when setting the `activeItems` or `selectedItems`.
* Instead we have to find the references of the monaco wrappers that contain the selected/active items
*/
protected findMonacoItemReferences(source: readonly (MonacoQuickPickItem<T> | IQuickPickSeparator)[], items: readonly QuickPickItem[]): MonacoQuickPickItem<T>[] {
const monacoReferences: MonacoQuickPickItem<T>[] = [];
for (const item of items) {
for (const wrappedItem of source) {
if (wrappedItem instanceof MonacoQuickPickItem && wrappedItem.item === item) {
monacoReferences.push(wrappedItem);
}
}
}
return monacoReferences;
}
}
export class MonacoQuickPickItem<T extends QuickPickItem> implements IQuickPickItem {
readonly type?: 'item';
readonly id?: string;
readonly label: string;
readonly meta?: string;
readonly ariaLabel?: string;
readonly description?: string;
readonly detail?: string;
readonly keybinding?: ResolvedKeybinding;
readonly iconClasses?: string[];
buttons?: IQuickInputButton[];
readonly alwaysShow?: boolean;
readonly highlights?: QuickPickItemHighlights;
constructor(readonly item: T, kbRegistry: KeybindingRegistry) {
this.type = item.type;
this.id = item.id;
this.label = item.label;
this.meta = item.meta;
this.ariaLabel = item.ariaLabel;
this.description = item.description;
this.detail = item.detail;
this.keybinding = item.keySequence ? new MonacoResolvedKeybinding(item.keySequence, kbRegistry) : undefined;
this.iconClasses = item.iconClasses;
this.buttons = item.buttons;
this.alwaysShow = item.alwaysShow;
this.highlights = item.highlights;
}
accept(): void {
if (this.item.execute) {
this.item.execute();
}
}
}

View File

@@ -0,0 +1,162 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { KeyCode as MonacoKeyCode } from '@theia/monaco-editor-core/esm/vs/base/common/keyCodes';
import {
ResolvedKeybinding, ResolvedChord, SingleModifierChord, KeyCodeChord, Chord
} from '@theia/monaco-editor-core/esm/vs/base/common/keybindings';
import { ElectronAcceleratorLabelProvider, UILabelProvider, UserSettingsLabelProvider } from '@theia/monaco-editor-core/esm/vs/base/common/keybindingLabels';
import { USLayoutResolvedKeybinding } from '@theia/monaco-editor-core/esm/vs/platform/keybinding/common/usLayoutResolvedKeybinding';
import * as MonacoPlatform from '@theia/monaco-editor-core/esm/vs/base/common/platform';
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
import { KeyCode, KeySequence, Keystroke, Key, KeyModifier } from '@theia/core/lib/browser/keys';
import { isOSX } from '@theia/core/lib/common/os';
import { KEY_CODE_MAP } from './monaco-keycode-map';
export class MonacoResolvedKeybinding extends ResolvedKeybinding {
protected readonly chords: ResolvedChord[];
constructor(protected readonly keySequence: KeySequence, keybindingService: KeybindingRegistry) {
super();
this.chords = keySequence.map(keyCode => {
// eslint-disable-next-line no-null/no-null
const keyLabel = keyCode.key ? keybindingService.acceleratorForKey(keyCode.key) : null;
const keyAriaLabel = keyLabel;
return new ResolvedChord(
keyCode.ctrl,
keyCode.shift,
keyCode.alt,
keyCode.meta,
keyLabel,
keyAriaLabel
);
});
}
getLabel(): string | null {
return UILabelProvider.toLabel(MonacoPlatform.OS, this.chords, p => p.keyLabel);
}
getAriaLabel(): string | null {
return UILabelProvider.toLabel(MonacoPlatform.OS, this.chords, p => p.keyAriaLabel);
}
getElectronAccelerator(): string | null {
if (this.hasMultipleChords()) {
// Electron cannot handle chords
// eslint-disable-next-line no-null/no-null
return null;
}
return ElectronAcceleratorLabelProvider.toLabel(MonacoPlatform.OS, this.chords, p => p.keyLabel);
}
getUserSettingsLabel(): string | null {
return UserSettingsLabelProvider.toLabel(MonacoPlatform.OS, this.chords, p => p.keyLabel);
}
isWYSIWYG(): boolean {
return true;
}
hasMultipleChords(): boolean {
return this.chords.length > 1;
}
getDispatchChords(): (string | null)[] {
return this.keySequence.map(keyCode => USLayoutResolvedKeybinding.getDispatchStr(this.toKeybinding(keyCode)));
}
getSingleModifierDispatchChords(): (SingleModifierChord | null)[] {
return this.keySequence.map(keybinding => this.getSingleModifierDispatchPart(keybinding));
}
protected getSingleModifierDispatchPart(code: KeyCode): SingleModifierChord | null {
if (code.key?.keyCode === undefined) {
return null; // eslint-disable-line no-null/no-null
}
if (KEY_CODE_MAP[code.key?.keyCode] === MonacoKeyCode.Ctrl && !code.shift && !code.alt && !code.meta) {
return 'ctrl';
}
if (KEY_CODE_MAP[code.key?.keyCode] === MonacoKeyCode.Shift && !code.ctrl && !code.alt && !code.meta) {
return 'shift';
}
if (KEY_CODE_MAP[code.key?.keyCode] === MonacoKeyCode.Alt && !code.shift && !code.ctrl && !code.meta) {
return 'alt';
}
if (KEY_CODE_MAP[code.key?.keyCode] === MonacoKeyCode.Meta && !code.shift && !code.alt && !code.ctrl) {
return 'meta';
}
return null; // eslint-disable-line no-null/no-null
}
private toKeybinding(keyCode: KeyCode): KeyCodeChord {
return new KeyCodeChord(
keyCode.ctrl,
keyCode.shift,
keyCode.alt,
keyCode.meta,
KEY_CODE_MAP[keyCode.key!.keyCode]
);
}
public getChords(): ResolvedChord[] {
return this.chords;
}
static toKeybinding(keybindings: Array<Chord>): string {
return keybindings.map(binding => this.keyCode(binding)).join(' ');
}
static keyCode(keybinding: Chord): KeyCode {
const keyCode = keybinding instanceof KeyCodeChord ? keybinding.keyCode : USLayoutResolvedKeybinding['_scanCodeToKeyCode'](keybinding.scanCode);
const sequence: Keystroke = {
first: Key.getKey(this.monaco2BrowserKeyCode(keyCode & 0xff)),
modifiers: []
};
if (keybinding.ctrlKey) {
if (isOSX) {
sequence.modifiers!.push(KeyModifier.MacCtrl);
} else {
sequence.modifiers!.push(KeyModifier.CtrlCmd);
}
}
if (keybinding.shiftKey) {
sequence.modifiers!.push(KeyModifier.Shift);
}
if (keybinding.altKey) {
sequence.modifiers!.push(KeyModifier.Alt);
}
if (keybinding.metaKey && sequence.modifiers!.indexOf(KeyModifier.CtrlCmd) === -1) {
sequence.modifiers!.push(KeyModifier.CtrlCmd);
}
return KeyCode.createKeyCode(sequence);
}
static keySequence(keybinding: Chord[]): KeySequence {
return keybinding.map(part => this.keyCode(part));
}
private static monaco2BrowserKeyCode(keyCode: MonacoKeyCode): number {
for (let i = 0; i < KEY_CODE_MAP.length; i++) {
if (KEY_CODE_MAP[i] === keyCode) {
return i;
}
}
return -1;
}
}

View File

@@ -0,0 +1,306 @@
// *****************************************************************************
// Copyright (C) 2019 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as jsoncparser from 'jsonc-parser';
import { injectable, inject } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { FileOperationError } from '@theia/filesystem/lib/common/files';
import * as monaco from '@theia/monaco-editor-core';
import { SnippetParser } from '@theia/monaco-editor-core/esm/vs/editor/contrib/snippet/browser/snippetParser';
import { isObject } from '@theia/core/lib/common';
@injectable()
export class MonacoSnippetSuggestProvider implements monaco.languages.CompletionItemProvider {
private static readonly _maxPrefix = 10000;
@inject(FileService)
protected readonly fileService: FileService;
protected readonly snippets = new Map<string, Snippet[]>();
protected readonly pendingSnippets = new Map<string, Promise<void>[]>();
async provideCompletionItems(model: monaco.editor.ITextModel, position: monaco.Position,
context: monaco.languages.CompletionContext): Promise<monaco.languages.CompletionList | undefined> {
// copied and modified from https://github.com/microsoft/vscode/blob/master/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts
if (position.column >= MonacoSnippetSuggestProvider._maxPrefix) {
return undefined;
}
if (context.triggerKind === monaco.languages.CompletionTriggerKind.TriggerCharacter && context.triggerCharacter === ' ') {
// no snippets when suggestions have been triggered by space
return undefined;
}
const languageId = model.getLanguageId(); // TODO: look up a language id at the position
await this.loadSnippets(languageId);
const snippetsForLanguage = this.snippets.get(languageId) || [];
const pos = { lineNumber: position.lineNumber, column: 1 };
const lineOffsets: number[] = [];
const linePrefixLow = model.getLineContent(position.lineNumber).substring(0, position.column - 1).toLowerCase();
const endsInWhitespace = linePrefixLow.match(/\s$/);
while (pos.column < position.column) {
const word = model.getWordAtPosition(pos);
if (word) {
// at a word
lineOffsets.push(word.startColumn - 1);
pos.column = word.endColumn + 1;
if (word.endColumn - 1 < linePrefixLow.length && !/\s/.test(linePrefixLow[word.endColumn - 1])) {
lineOffsets.push(word.endColumn - 1);
}
} else if (!/\s/.test(linePrefixLow[pos.column - 1])) {
// at a none-whitespace character
lineOffsets.push(pos.column - 1);
pos.column += 1;
} else {
// always advance!
pos.column += 1;
}
}
const availableSnippets = new Set<Snippet>();
snippetsForLanguage.forEach(availableSnippets.add, availableSnippets);
const suggestions: MonacoSnippetSuggestion[] = [];
for (const start of lineOffsets) {
availableSnippets.forEach(snippet => {
if (this.isPatternInWord(linePrefixLow, start, linePrefixLow.length, snippet.prefix.toLowerCase(), 0, snippet.prefix.length)) {
suggestions.push(new MonacoSnippetSuggestion(snippet, monaco.Range.fromPositions(position.delta(0, -(linePrefixLow.length - start)), position)));
availableSnippets.delete(snippet);
}
});
}
if (endsInWhitespace || lineOffsets.length === 0) {
// add remaining snippets when the current prefix ends in whitespace or when no
// interesting positions have been found
availableSnippets.forEach(snippet => {
suggestions.push(new MonacoSnippetSuggestion(snippet, monaco.Range.fromPositions(position)));
});
}
// disambiguate suggestions with same labels
suggestions.sort(MonacoSnippetSuggestion.compareByLabel);
return { suggestions };
}
resolveCompletionItem?(item: monaco.languages.CompletionItem, token: monaco.CancellationToken): monaco.languages.CompletionItem {
return item instanceof MonacoSnippetSuggestion ? item.resolve() : item;
}
protected async loadSnippets(scope: string): Promise<void> {
const pending: Promise<void>[] = [];
pending.push(...(this.pendingSnippets.get(scope) || []));
pending.push(...(this.pendingSnippets.get('*') || []));
if (pending.length) {
await Promise.all(pending);
}
}
fromURI(uri: string | URI, options: SnippetLoadOptions): Disposable {
const toDispose = new DisposableCollection(Disposable.create(() => { /* mark as not disposed */ }));
const pending = this.loadURI(uri, options, toDispose);
const { language } = options;
const scopes = Array.isArray(language) ? language : !!language ? [language] : ['*'];
for (const scope of scopes) {
const pendingSnippets = this.pendingSnippets.get(scope) || [];
pendingSnippets.push(pending);
this.pendingSnippets.set(scope, pendingSnippets);
toDispose.push(Disposable.create(() => {
const index = pendingSnippets.indexOf(pending);
if (index !== -1) {
pendingSnippets.splice(index, 1);
}
}));
}
return toDispose;
}
/**
* should NOT throw to prevent load errors on suggest
*/
protected async loadURI(uri: string | URI, options: SnippetLoadOptions, toDispose: DisposableCollection): Promise<void> {
try {
const resource = typeof uri === 'string' ? new URI(uri) : uri;
const { value } = await this.fileService.read(resource);
if (toDispose.disposed) {
return;
}
const snippets = value && jsoncparser.parse(value, undefined, { disallowComments: false });
toDispose.push(this.fromJSON(snippets, options));
} catch (e) {
if (!(e instanceof FileOperationError)) {
console.error(e);
}
}
}
fromJSON(snippets: JsonSerializedSnippets | undefined, { language, source }: SnippetLoadOptions): Disposable {
const toDispose = new DisposableCollection();
this.parseSnippets(snippets, (name, snippet) => {
const { isFileTemplate, prefix, body, description } = snippet;
const parsedBody = Array.isArray(body) ? body.join('\n') : body;
const parsedPrefixes = !prefix ? [''] : Array.isArray(prefix) ? prefix : [prefix];
if (typeof parsedBody !== 'string') {
return;
}
const scopes: string[] = [];
if (language) {
if (Array.isArray(language)) {
scopes.push(...language);
} else {
scopes.push(language);
}
} else if (typeof snippet.scope === 'string') {
for (const rawScope of snippet.scope.split(',')) {
const scope = rawScope.trim();
if (scope) {
scopes.push(scope);
}
}
}
parsedPrefixes.forEach(parsedPrefix => toDispose.push(this.push({
isFileTemplate: Boolean(isFileTemplate),
scopes,
name,
prefix: parsedPrefix,
description,
body: parsedBody,
source
})));
});
return toDispose;
}
protected parseSnippets(snippets: JsonSerializedSnippets | undefined, accept: (name: string, snippet: JsonSerializedSnippet) => void): void {
for (const [name, scopeOrTemplate] of Object.entries(snippets ?? {})) {
if (JsonSerializedSnippet.is(scopeOrTemplate)) {
accept(name, scopeOrTemplate);
} else {
// eslint-disable-next-line @typescript-eslint/no-shadow
for (const [name, template] of Object.entries(scopeOrTemplate)) {
accept(name, template);
}
}
}
}
push(...snippets: Snippet[]): Disposable {
const toDispose = new DisposableCollection();
for (const snippet of snippets) {
for (const scope of snippet.scopes) {
const languageSnippets = this.snippets.get(scope) || [];
languageSnippets.push(snippet);
this.snippets.set(scope, languageSnippets);
toDispose.push(Disposable.create(() => {
const index = languageSnippets.indexOf(snippet);
if (index !== -1) {
languageSnippets.splice(index, 1);
}
}));
}
}
return toDispose;
}
protected isPatternInWord(patternLow: string, patternPos: number, patternLen: number, wordLow: string, wordPos: number, wordLen: number): boolean {
while (patternPos < patternLen && wordPos < wordLen) {
if (patternLow[patternPos] === wordLow[wordPos]) {
patternPos += 1;
}
wordPos += 1;
}
return patternPos === patternLen; // pattern must be exhausted
}
}
export interface SnippetLoadOptions {
language?: string | string[]
source: string
}
export interface JsonSerializedSnippets {
[name: string]: JsonSerializedSnippet | { [name: string]: JsonSerializedSnippet };
}
export interface JsonSerializedSnippet {
isFileTemplate?: boolean;
body: string | string[];
scope?: string;
prefix?: string | string[];
description: string;
}
export namespace JsonSerializedSnippet {
export function is(obj: unknown): obj is JsonSerializedSnippet {
return isObject(obj) && 'body' in obj;
}
}
export interface Snippet {
readonly isFileTemplate: boolean
readonly scopes: string[]
readonly name: string
readonly prefix: string
readonly description: string
readonly body: string
readonly source: string
}
export class MonacoSnippetSuggestion implements monaco.languages.CompletionItem {
readonly label: string;
readonly detail: string;
readonly sortText: string;
readonly noAutoAccept = true;
readonly kind = monaco.languages.CompletionItemKind.Snippet;
readonly insertTextRules = monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet;
insertText: string;
documentation?: monaco.IMarkdownString;
constructor(
protected readonly snippet: Snippet,
readonly range: monaco.Range
) {
this.label = snippet.prefix;
this.detail = `${snippet.description || snippet.name} (${snippet.source})`;
this.insertText = snippet.body;
this.sortText = `z-${snippet.prefix}`;
this.range = range;
}
protected resolved = false;
resolve(): MonacoSnippetSuggestion {
if (!this.resolved) {
const codeSnippet = new SnippetParser().parse(this.snippet.body).toString();
this.documentation = { value: '```\n' + codeSnippet + '```' };
this.resolved = true;
}
return this;
}
static compareByLabel(a: MonacoSnippetSuggestion, b: MonacoSnippetSuggestion): number {
return a.label > b.label ? 1 : a.label < b.label ? -1 : 0;
}
}

View File

@@ -0,0 +1,51 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// *****************************************************************************
// Copyright (C) 2024 STMicroelectronics and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { IDisposable } from '@theia/monaco-editor-core/esm/vs/base/common/lifecycle';
import { StandaloneThemeService } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneThemeService';
export class MonacoStandaloneThemeService extends StandaloneThemeService {
protected get styleElements(): HTMLStyleElement[] {
// access private style element array
return (this as any)._styleElements;
}
protected get allCSS(): string {
return (this as any)._allCSS;
}
override registerEditorContainer(domNode: HTMLElement): IDisposable {
const style = domNode.ownerDocument.createElement('style');
style.type = 'text/css';
style.media = 'screen';
style.className = 'monaco-colors';
style.textContent = this.allCSS;
domNode.ownerDocument.head.appendChild(style);
this.styleElements.push(style);
return {
dispose: () => {
for (let i = 0; i < this.styleElements.length; i++) {
if (this.styleElements[i] === style) {
this.styleElements.splice(i, 1);
style.remove();
return;
}
}
}
};
}
}

View File

@@ -0,0 +1,107 @@
// *****************************************************************************
// Copyright (C) 2018 Ericsson
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { DisposableCollection, nls } from '@theia/core';
import { StatusBar, StatusBarAlignment, Widget, WidgetStatusBarContribution } from '@theia/core/lib/browser';
import { EditorCommands, EditorWidget } from '@theia/editor/lib/browser';
import { MonacoEditor } from './monaco-editor';
import * as monaco from '@theia/monaco-editor-core';
export const EDITOR_STATUS_TABBING_CONFIG = 'editor-status-tabbing-config';
export const EDITOR_STATUS_EOL = 'editor-status-eol';
@injectable()
export class MonacoStatusBarContribution implements WidgetStatusBarContribution<EditorWidget> {
protected readonly toDispose = new DisposableCollection();
canHandle(widget: Widget): widget is EditorWidget {
if (widget instanceof EditorWidget) {
return Boolean(this.getModel(widget));
}
return false;
}
activate(statusBar: StatusBar, editor: EditorWidget): void {
this.toDispose.dispose();
const editorModel = this.getModel(editor);
if (editorModel) {
this.setConfigTabSizeWidget(statusBar, editorModel);
this.setLineEndingWidget(statusBar, editorModel);
this.toDispose.push(editorModel.onDidChangeOptions(() => {
this.setConfigTabSizeWidget(statusBar, editorModel);
this.setLineEndingWidget(statusBar, editorModel);
}));
let previous = editorModel.getEOL();
this.toDispose.push(editorModel.onDidChangeContent(e => {
if (previous !== e.eol) {
previous = e.eol;
this.setLineEndingWidget(statusBar, editorModel);
}
}));
} else {
this.deactivate(statusBar);
}
}
deactivate(statusBar: StatusBar): void {
this.toDispose.dispose();
this.removeConfigTabSizeWidget(statusBar);
this.removeLineEndingWidget(statusBar);
}
protected setConfigTabSizeWidget(statusBar: StatusBar, model: monaco.editor.ITextModel): void {
const modelOptions = model.getOptions();
const tabSize = modelOptions.tabSize;
const indentSize = modelOptions.indentSize;
const spaceOrTabSizeMessage = modelOptions.insertSpaces
? nls.localizeByDefault('Spaces: {0}', indentSize)
: nls.localizeByDefault('Tab Size: {0}', tabSize);
statusBar.setElement(EDITOR_STATUS_TABBING_CONFIG, {
text: spaceOrTabSizeMessage,
alignment: StatusBarAlignment.RIGHT,
priority: 10,
command: EditorCommands.CONFIG_INDENTATION.id,
tooltip: nls.localizeByDefault('Select Indentation')
});
}
protected removeConfigTabSizeWidget(statusBar: StatusBar): void {
statusBar.removeElement(EDITOR_STATUS_TABBING_CONFIG);
}
protected setLineEndingWidget(statusBar: StatusBar, model: monaco.editor.ITextModel): void {
const eol = model.getEOL();
const text = eol === '\n' ? 'LF' : 'CRLF';
statusBar.setElement(EDITOR_STATUS_EOL, {
text: `${text}`,
alignment: StatusBarAlignment.RIGHT,
priority: 11,
command: EditorCommands.CONFIG_EOL.id,
tooltip: nls.localizeByDefault('Select End of Line Sequence')
});
}
protected removeLineEndingWidget(statusBar: StatusBar): void {
statusBar.removeElement(EDITOR_STATUS_EOL);
}
protected getModel(editor: EditorWidget | undefined): monaco.editor.ITextModel | undefined {
const monacoEditor = MonacoEditor.get(editor);
return monacoEditor && monacoEditor.getControl().getModel() || undefined;
}
}

View File

@@ -0,0 +1,183 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { ResourceProvider, ReferenceCollection, Event, MaybePromise, Resource, ContributionProvider, OS, Emitter } from '@theia/core';
import { EditorPreferences, EditorPreferenceChange } from '@theia/editor/lib/common/editor-preferences';
import { MonacoEditorModel } from './monaco-editor-model';
import { IDisposable, IReference } from '@theia/monaco-editor-core/esm/vs/base/common/lifecycle';
import { MonacoToProtocolConverter } from './monaco-to-protocol-converter';
import { ProtocolToMonacoConverter } from './protocol-to-monaco-converter';
import { ILogger } from '@theia/core/lib/common/logger';
import * as monaco from '@theia/monaco-editor-core';
import { ITextModelService, ITextModelContentProvider } from '@theia/monaco-editor-core/esm/vs/editor/common/services/resolverService';
import { ITextModelUpdateOptions } from '@theia/monaco-editor-core/esm/vs/editor/common/model';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
import { ITextResourcePropertiesService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/textResourceConfiguration';
export const MonacoEditorModelFactory = Symbol('MonacoEditorModelFactory');
export interface MonacoEditorModelFactory {
readonly scheme: string;
createModel(
resource: Resource
): MaybePromise<MonacoEditorModel>;
}
export const MonacoEditorModelFilter = Symbol('MonacoEditorModelFilter');
/**
* A filter that prevents firing the `onDidCreate` event for certain models.
* Preventing this event from firing will also prevent the propagation of the model to the plugin host.
*
* This is useful for models that are not supposed to be opened in a dedicated monaco editor widgets.
* This includes models for notebook cells.
*/
export interface MonacoEditorModelFilter {
/**
* Return `true` on models that should be filtered.
*/
filter(model: MonacoEditorModel): boolean;
}
@injectable()
export class MonacoTextModelService implements ITextModelService {
declare readonly _serviceBrand: undefined;
protected readonly _models = new ReferenceCollection<string, MonacoEditorModel>(
uri => this.loadModel(new URI(uri))
);
protected readonly _visibleModels = new Set<MonacoEditorModel>();
protected readonly onDidCreateEmitter = new Emitter<MonacoEditorModel>();
@inject(ResourceProvider)
protected readonly resourceProvider: ResourceProvider;
@inject(EditorPreferences)
protected readonly editorPreferences: EditorPreferences;
@inject(MonacoToProtocolConverter)
protected readonly m2p: MonacoToProtocolConverter;
@inject(ProtocolToMonacoConverter)
protected readonly p2m: ProtocolToMonacoConverter;
@inject(ContributionProvider)
@named(MonacoEditorModelFactory)
protected readonly factories: ContributionProvider<MonacoEditorModelFactory>;
@inject(ContributionProvider)
@named(MonacoEditorModelFilter)
protected readonly filters: ContributionProvider<MonacoEditorModelFilter>;
@inject(ILogger)
protected readonly logger: ILogger;
@inject(FileService)
protected readonly fileService: FileService;
@postConstruct()
protected init(): void {
const resourcePropertiesService = StandaloneServices.get(ITextResourcePropertiesService);
if (resourcePropertiesService) {
resourcePropertiesService.getEOL = () => {
const eol = this.editorPreferences['files.eol'];
if (eol && eol !== 'auto') {
return eol;
}
return OS.backend.EOL;
};
}
this._models.onDidCreate(model => {
const filters = this.filters.getContributions();
if (filters.some(filter => filter.filter(model))) {
return;
}
this._visibleModels.add(model);
const dispose = model.onWillDispose(() => {
this._visibleModels.delete(model);
dispose.dispose();
});
this.onDidCreateEmitter.fire(model);
});
}
get models(): MonacoEditorModel[] {
return Array.from(this._visibleModels);
}
get(uri: string): MonacoEditorModel | undefined {
return this._models.get(uri);
}
get onDidCreate(): Event<MonacoEditorModel> {
return this.onDidCreateEmitter.event;
}
createModelReference(raw: monaco.Uri | URI): Promise<IReference<MonacoEditorModel>> {
return this._models.acquire(raw.toString());
}
async loadModel(uri: URI): Promise<MonacoEditorModel> {
await this.editorPreferences.ready;
const resource = await this.resourceProvider(uri);
const model = await (await this.createModel(resource)).load();
return model;
}
protected createModel(resource: Resource): MaybePromise<MonacoEditorModel> {
const factory = this.factories.getContributions().find(({ scheme }) => resource.uri.scheme === scheme);
return factory ? factory.createModel(resource) : new MonacoEditorModel(resource, this.m2p, this.p2m, this.logger, this.editorPreferences);
}
protected readonly modelOptions: { [name: string]: (keyof ITextModelUpdateOptions | undefined) } = {
'editor.tabSize': 'tabSize',
'editor.insertSpaces': 'insertSpaces',
'editor.indentSize': 'indentSize'
};
protected toModelOption(editorPreference: EditorPreferenceChange['preferenceName']): keyof ITextModelUpdateOptions | undefined {
switch (editorPreference) {
case 'editor.tabSize': return 'tabSize';
case 'editor.indentSize': return 'indentSize';
case 'editor.insertSpaces': return 'insertSpaces';
case 'editor.bracketPairColorization.enabled':
case 'editor.bracketPairColorization.independentColorPoolPerBracketType':
return 'bracketColorizationOptions';
case 'editor.trimAutoWhitespace': return 'trimAutoWhitespace';
}
return undefined;
}
registerTextModelContentProvider(scheme: string, provider: ITextModelContentProvider): IDisposable {
return {
dispose(): void {
// no-op
}
};
}
canHandleResource(resource: monaco.Uri): boolean {
return this.fileService.canHandleResource(URI.fromComponents(resource));
}
}

View File

@@ -0,0 +1,204 @@
// *****************************************************************************
// Copyright (C) 2019 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
/* eslint-disable @typescript-eslint/no-explicit-any */
import { injectable, inject } from '@theia/core/shared/inversify';
import * as jsoncparser from 'jsonc-parser';
import * as plistparser from 'fast-plist';
import URI from '@theia/core/lib/common/uri';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { MonacoThemeRegistry } from './textmate/monaco-theme-registry';
import { getThemes, putTheme, MonacoThemeState, stateToTheme, ThemeServiceWithDB } from './monaco-indexed-db';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import * as monaco from '@theia/monaco-editor-core';
export interface MonacoTheme {
id?: string;
label?: string;
uiTheme?: monaco.editor.BuiltinTheme;
description?: string;
uri: string;
}
export interface MonacoThemeJson {
/**
* theme id (optional), label is used if not provided
*/
id?: string;
label: string;
/**
* theme type, `vs-dark` if not provided
*/
uiTheme?: monaco.editor.BuiltinTheme;
description?: string;
/**
* Follow https://code.visualstudio.com/api/extension-guides/color-theme#create-a-new-color-theme to create a custom theme.
*/
json: any
/**
* Themes can include each other. It specifies how inclusions should be resolved.
*/
includes?: { [includePath: string]: any }
}
@injectable()
export class MonacoThemingService {
@inject(FileService) protected readonly fileService: FileService;
@inject(MonacoThemeRegistry) protected readonly monacoThemeRegistry: MonacoThemeRegistry;
@inject(ThemeServiceWithDB) protected readonly themeService: ThemeServiceWithDB;
/** Register themes whose configuration needs to be loaded */
register(theme: MonacoTheme, pending: { [uri: string]: Promise<any> } = {}): Disposable {
const toDispose = new DisposableCollection(Disposable.create(() => { /* mark as not disposed */ }));
this.doRegister(theme, pending, toDispose);
return toDispose;
}
protected async doRegister(theme: MonacoTheme,
pending: { [uri: string]: Promise<any> },
toDispose: DisposableCollection
): Promise<void> {
try {
const includes = {};
const json = await this.loadTheme(theme.uri, includes, pending, toDispose);
if (toDispose.disposed) {
return;
}
const label = theme.label || new URI(theme.uri).path.base;
const { id, description, uiTheme } = theme;
toDispose.push(this.registerParsedTheme({ id, label, description, uiTheme: uiTheme, json, includes }));
} catch (e) {
console.error('Failed to load theme from ' + theme.uri, e);
}
}
protected async loadTheme(
uri: string,
includes: { [include: string]: any },
pending: { [uri: string]: Promise<any> },
toDispose: DisposableCollection
): Promise<any> {
const result = await this.fileService.read(new URI(uri));
const content = result.value;
if (toDispose.disposed) {
return;
}
const themeUri = new URI(uri);
if (themeUri.path.ext !== '.json') {
const value = plistparser.parse(content);
if (value && 'settings' in value && Array.isArray(value.settings)) {
return { tokenColors: value.settings };
}
throw new Error(`Problem parsing tmTheme file: ${uri}. 'settings' is not array.`);
}
const json = jsoncparser.parse(content, undefined, { disallowComments: false });
if ('tokenColors' in json && typeof json.tokenColors === 'string') {
const value = await this.doLoadTheme(themeUri, json.tokenColors, includes, pending, toDispose);
if (toDispose.disposed) {
return;
}
json.tokenColors = value.tokenColors;
}
if (json.include) {
includes[json.include] = await this.doLoadTheme(themeUri, json.include, includes, pending, toDispose);
if (toDispose.disposed) {
return;
}
}
this.clean(json.colors);
return json;
}
protected doLoadTheme(
themeUri: URI,
referencedPath: string,
includes: { [include: string]: any },
pending: { [uri: string]: Promise<any> },
toDispose: DisposableCollection
): Promise<any> {
const referencedUri = themeUri.parent.resolve(referencedPath).toString();
if (!pending[referencedUri]) {
pending[referencedUri] = this.loadTheme(referencedUri, includes, pending, toDispose);
}
return pending[referencedUri];
}
initialize(): void {
this.monacoThemeRegistry.initializeDefaultThemes();
this.updateBodyUiTheme();
this.themeService.onDidColorThemeChange(() => this.updateBodyUiTheme());
this.themeService.onDidRetrieveTheme(theme => this.monacoThemeRegistry.setTheme(MonacoThemingService.toCssSelector(theme.id), theme.data));
this.restore();
}
/** register a theme whose configuration has already been loaded */
registerParsedTheme(theme: MonacoThemeJson): Disposable {
const uiTheme = theme.uiTheme || 'vs-dark';
const { label, description, json, includes } = theme;
const id = theme.id || label;
const cssSelector = MonacoThemingService.toCssSelector(id);
const data = this.monacoThemeRegistry.register(json, includes, cssSelector, uiTheme);
return this.doRegisterParsedTheme({ id, label, description, uiTheme, data });
}
protected toUpdateUiTheme = new DisposableCollection();
protected updateBodyUiTheme(): void {
this.toUpdateUiTheme.dispose();
const type = this.themeService.getCurrentTheme().type;
const uiTheme: monaco.editor.BuiltinTheme = type === 'hc' ? 'hc-black' : type === 'light' ? 'vs' : 'vs-dark';
document.body.classList.add(uiTheme);
this.toUpdateUiTheme.push(Disposable.create(() => document.body.classList.remove(uiTheme)));
}
protected doRegisterParsedTheme(state: MonacoThemeState): Disposable {
return new DisposableCollection(
this.themeService.register(stateToTheme(state)),
putTheme(state)
);
}
protected async restore(): Promise<void> {
try {
const themes = await getThemes();
for (const state of themes) {
this.monacoThemeRegistry.setTheme(state.data.name!, state.data);
this.doRegisterParsedTheme(state);
}
} catch (e) {
console.error('Failed to restore monaco themes', e);
}
}
/* remove all characters that are not allowed in css */
protected static toCssSelector(str: string): string {
str = str.replace(/[^\-a-zA-Z0-9]/g, '-');
if (str.charAt(0).match(/[0-9\-]/)) {
str = '-' + str;
}
return str;
}
/** removes all invalid theming values */
private clean(obj: any): void {
for (const key in obj) {
if (typeof obj[key] !== 'string') {
delete obj[key];
}
}
}
}

View File

@@ -0,0 +1,89 @@
// *****************************************************************************
// Copyright (C) 2020 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { Position, Range } from '@theia/core/shared/vscode-languageserver-protocol';
import { RecursivePartial } from '@theia/core/lib/common/types';
import * as monaco from '@theia/monaco-editor-core';
import { Selection } from '@theia/editor/lib/browser';
export interface MonacoRangeReplace {
insert: monaco.IRange;
replace: monaco.IRange
};
export namespace MonacoRangeReplace {
export function is(v: Partial<monaco.IRange> | MonacoRangeReplace): v is MonacoRangeReplace {
return (v as MonacoRangeReplace).insert !== undefined;
}
}
@injectable()
export class MonacoToProtocolConverter {
asPosition(lineNumber: undefined | null, column: undefined | null): {};
asPosition(lineNumber: number, column: undefined | null): Pick<Position, 'line'>;
asPosition(lineNumber: undefined | null, column: number): Pick<Position, 'character'>;
asPosition(lineNumber: number, column: number): Position;
asPosition(lineNumber: number | undefined | null, column: number | undefined | null): Partial<Position>;
asPosition(lineNumber: number | undefined | null, column: number | undefined | null): Partial<Position> {
const line = typeof lineNumber !== 'number' ? undefined : lineNumber - 1;
const character = typeof column !== 'number' ? undefined : column - 1;
return {
line, character
};
}
asRange(range: undefined): undefined;
asRange(range: monaco.IRange): Range;
asRange(range: monaco.IRange | undefined): Range | undefined;
asRange(range: monaco.IRange | { insert: monaco.IRange; replace: monaco.IRange }): Range;
asRange(range: Partial<monaco.IRange>): RecursivePartial<Range>;
asRange(range: Partial<monaco.IRange> | undefined): RecursivePartial<Range> | undefined;
asRange(range: Partial<monaco.IRange> | undefined | MonacoRangeReplace): RecursivePartial<Range> | undefined {
if (range === undefined) {
return undefined;
}
if (MonacoRangeReplace.is(range)) {
return this.asRange(range.insert);
} else {
const start = this.asPosition(range.startLineNumber, range.startColumn);
const end = this.asPosition(range.endLineNumber, range.endColumn);
return {
start, end
};
}
}
asSelection(selection: monaco.Selection | null): Selection {
if (!selection) {
return {
start: { line: 0, character: 0 },
end: { line: 0, character: 0 },
direction: 'ltr'
};
}
const start = this.asPosition(selection.selectionStartLineNumber, selection.selectionStartColumn);
const end = this.asPosition(selection.positionLineNumber, selection.positionColumn);
return {
start,
end,
direction: selection.getDirection() === monaco.SelectionDirection.LTR ? 'ltr' : 'rtl'
};
}
}

View File

@@ -0,0 +1,64 @@
// *****************************************************************************
// Copyright (C) 2024 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { UndoRedoHandler } from '@theia/core/lib/browser';
import { injectable } from '@theia/core/shared/inversify';
import { ICodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorBrowser';
import { ICodeEditorService } from '@theia/monaco-editor-core/esm/vs/editor/browser/services/codeEditorService';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
@injectable()
export abstract class AbstractMonacoUndoRedoHandler implements UndoRedoHandler<ICodeEditor> {
priority: number;
abstract select(): ICodeEditor | undefined;
undo(item: ICodeEditor): void {
item.trigger('MonacoUndoRedoHandler', 'undo', undefined);
}
redo(item: ICodeEditor): void {
item.trigger('MonacoUndoRedoHandler', 'redo', undefined);
}
}
@injectable()
export class FocusedMonacoUndoRedoHandler extends AbstractMonacoUndoRedoHandler {
override priority = 10000;
protected codeEditorService = StandaloneServices.get(ICodeEditorService);
override select(): ICodeEditor | undefined {
const focusedEditor = this.codeEditorService.getFocusedCodeEditor();
if (focusedEditor && focusedEditor.hasTextFocus()) {
return focusedEditor;
}
return undefined;
}
}
@injectable()
export class ActiveMonacoUndoRedoHandler extends AbstractMonacoUndoRedoHandler {
override priority = 0;
protected codeEditorService = StandaloneServices.get(ICodeEditorService);
override select(): ICodeEditor | undefined {
const focusedEditor = this.codeEditorService.getActiveCodeEditor();
if (focusedEditor) {
focusedEditor.focus();
return focusedEditor;
}
return undefined;
}
}

View File

@@ -0,0 +1,46 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { MonacoEditorModel } from './monaco-editor-model';
export function insertFinalNewline(editorModel: MonacoEditorModel): void {
const model = editorModel.textEditorModel;
if (!model) {
return;
}
const lines = model?.getLineCount();
if (lines === 0) {
return;
}
const lastLine = model?.getLineContent(lines);
if (lastLine.trim() === '') {
return;
}
const lastLineMaxColumn = model?.getLineMaxColumn(lines);
const range = {
startLineNumber: lines,
startColumn: lastLineMaxColumn,
endLineNumber: lines,
endColumn: lastLineMaxColumn
};
model.applyEdits([{
range,
text: model?.getEOL()
}]);
}

View File

@@ -0,0 +1,79 @@
// *****************************************************************************
// Copyright (C) 2025 1C-Soft LLC and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/monaco-editor-core/esm/vs/base/common/event';
import { URI } from '@theia/monaco-editor-core/esm/vs/base/common/uri';
import {
ISingleFolderWorkspaceIdentifier,
IWorkspace,
IWorkspaceContextService,
IWorkspaceFolder,
IWorkspaceFoldersChangeEvent,
IWorkspaceFoldersWillChangeEvent,
IWorkspaceIdentifier,
UNKNOWN_EMPTY_WINDOW_WORKSPACE,
WorkbenchState
} from '@theia/monaco-editor-core/esm/vs/platform/workspace/common/workspace';
/**
* A minimal implementation of {@link IWorkspaceContextService} to replace the `StandaloneWorkspaceContextService` in Monaco
* as a workaround for the issue of showing no context menu for editor minimap (#15217).
*/
@injectable()
export class MonacoWorkspaceContextService implements IWorkspaceContextService {
declare readonly _serviceBrand: undefined;
protected readonly onDidChangeWorkbenchStateEmitter = new Emitter<WorkbenchState>();
readonly onDidChangeWorkbenchState = this.onDidChangeWorkbenchStateEmitter.event;
protected readonly onDidChangeWorkspaceNameEmitter = new Emitter<void>();
readonly onDidChangeWorkspaceName = this.onDidChangeWorkspaceNameEmitter.event;
protected readonly onWillChangeWorkspaceFoldersEmitter = new Emitter<IWorkspaceFoldersWillChangeEvent>();
readonly onWillChangeWorkspaceFolders = this.onWillChangeWorkspaceFoldersEmitter.event;
protected readonly onDidChangeWorkspaceFoldersEmitter = new Emitter<IWorkspaceFoldersChangeEvent>();
readonly onDidChangeWorkspaceFolders = this.onDidChangeWorkspaceFoldersEmitter.event;
protected workspace: IWorkspace = { id: UNKNOWN_EMPTY_WINDOW_WORKSPACE.id, folders: [] };
getCompleteWorkspace(): Promise<IWorkspace> {
return Promise.resolve(this.getWorkspace());
}
getWorkspace(): IWorkspace {
return this.workspace;
}
getWorkbenchState(): WorkbenchState {
return WorkbenchState.EMPTY;
}
getWorkspaceFolder(resource: URI): IWorkspaceFolder | null {
// eslint-disable-next-line no-null/no-null
return null;
}
isCurrentWorkspace(workspaceIdOrFolder: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI): boolean {
return false;
}
isInsideWorkspace(resource: URI): boolean {
return false;
}
}

View File

@@ -0,0 +1,411 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
/* eslint-disable no-null/no-null */
import { URI as Uri } from '@theia/core/shared/vscode-uri';
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { Emitter } from '@theia/core/lib/common/event';
import { FileSystemPreferences } from '@theia/filesystem/lib/common';
import { EditorManager } from '@theia/editor/lib/browser';
import { MonacoTextModelService } from './monaco-text-model-service';
import { MonacoEditorModel, MonacoModelContentChangedEvent } from './monaco-editor-model';
import { MonacoEditor } from './monaco-editor';
import { ProblemManager } from '@theia/markers/lib/browser';
import { ArrayUtils } from '@theia/core/lib/common/types';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { FileSystemProviderCapabilities } from '@theia/filesystem/lib/common/files';
import * as monaco from '@theia/monaco-editor-core';
import {
IBulkEditOptions,
IBulkEditResult, ResourceEdit, ResourceFileEdit as MonacoResourceFileEdit,
ResourceTextEdit as MonacoResourceTextEdit
} from '@theia/monaco-editor-core/esm/vs/editor/browser/services/bulkEditService';
import { IEditorWorkerService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/editorWorker';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
import { EndOfLineSequence } from '@theia/monaco-editor-core/esm/vs/editor/common/model';
import { SnippetParser } from '@theia/monaco-editor-core/esm/vs/editor/contrib/snippet/browser/snippetParser';
import { TextEdit } from '@theia/monaco-editor-core/esm/vs/editor/common/languages';
import { SnippetController2 } from '@theia/monaco-editor-core/esm/vs/editor/contrib/snippet/browser/snippetController2';
import { isObject, MaybePromise, nls } from '@theia/core/lib/common';
import { SaveableService } from '@theia/core/lib/browser';
import { EditorPreferences } from '@theia/editor/lib/common/editor-preferences';
export namespace WorkspaceFileEdit {
export function is(arg: Edit): arg is monaco.languages.IWorkspaceFileEdit {
return ('oldResource' in arg && monaco.Uri.isUri(arg.oldResource)) ||
('newResource' in arg && monaco.Uri.isUri(arg.newResource));
}
}
export namespace WorkspaceTextEdit {
export function is(arg: Edit): arg is monaco.languages.IWorkspaceTextEdit {
return isObject<monaco.languages.IWorkspaceTextEdit>(arg)
&& monaco.Uri.isUri(arg.resource)
&& isObject(arg.textEdit);
}
}
export type Edit = monaco.languages.IWorkspaceFileEdit | monaco.languages.IWorkspaceTextEdit;
export namespace ResourceFileEdit {
export function is(arg: ResourceEdit): arg is MonacoResourceFileEdit {
return isObject<MonacoResourceFileEdit>(arg) && (monaco.Uri.isUri(arg.oldResource) || monaco.Uri.isUri(arg.newResource));
}
}
export namespace ResourceTextEdit {
export function is(arg: ResourceEdit): arg is MonacoResourceTextEdit {
return ('resource' in arg && monaco.Uri.isUri((arg as MonacoResourceTextEdit).resource));
}
}
export interface WorkspaceFoldersChangeEvent {
readonly added: WorkspaceFolder[];
readonly removed: WorkspaceFolder[];
}
export interface WorkspaceFolder {
readonly uri: Uri;
readonly name: string;
readonly index: number;
}
@injectable()
export class MonacoWorkspace {
protected resolveReady: () => void;
readonly ready = new Promise<void>(resolve => {
this.resolveReady = resolve;
});
protected readonly onDidOpenTextDocumentEmitter = new Emitter<MonacoEditorModel>();
readonly onDidOpenTextDocument = this.onDidOpenTextDocumentEmitter.event;
protected readonly onDidCloseTextDocumentEmitter = new Emitter<MonacoEditorModel>();
readonly onDidCloseTextDocument = this.onDidCloseTextDocumentEmitter.event;
protected readonly onDidChangeTextDocumentEmitter = new Emitter<MonacoModelContentChangedEvent>();
readonly onDidChangeTextDocument = this.onDidChangeTextDocumentEmitter.event;
protected readonly onDidSaveTextDocumentEmitter = new Emitter<MonacoEditorModel>();
readonly onDidSaveTextDocument = this.onDidSaveTextDocumentEmitter.event;
@inject(FileService)
protected readonly fileService: FileService;
@inject(FileSystemPreferences)
protected readonly filePreferences: FileSystemPreferences;
@inject(EditorPreferences)
protected readonly editorPreferences: EditorPreferences;
@inject(MonacoTextModelService)
protected readonly textModelService: MonacoTextModelService;
@inject(EditorManager)
protected readonly editorManager: EditorManager;
@inject(ProblemManager)
protected readonly problems: ProblemManager;
@inject(SaveableService)
protected readonly saveService: SaveableService;
@postConstruct()
protected init(): void {
this.resolveReady();
for (const model of this.textModelService.models) {
this.fireDidOpen(model);
}
this.textModelService.onDidCreate(model => this.fireDidOpen(model));
}
get textDocuments(): MonacoEditorModel[] {
return this.textModelService.models;
}
getTextDocument(uri: string): MonacoEditorModel | undefined {
return this.textModelService.get(uri);
}
protected fireDidOpen(model: MonacoEditorModel): void {
this.doFireDidOpen(model);
model.textEditorModel.onDidChangeLanguage(e => {
this.problems.cleanAllMarkers(new URI(model.uri));
model.setLanguageId(e.oldLanguage);
try {
this.fireDidClose(model);
} finally {
model.setLanguageId(undefined);
}
this.doFireDidOpen(model);
});
model.onDidChangeContent(event => this.fireDidChangeContent(event));
model.onDidSaveModel(() => this.fireDidSave(model));
model.onDirtyChanged(() => this.openEditorIfDirty(model));
model.onDispose(() => this.fireDidClose(model));
}
protected doFireDidOpen(model: MonacoEditorModel): void {
this.onDidOpenTextDocumentEmitter.fire(model);
}
protected fireDidClose(model: MonacoEditorModel): void {
this.onDidCloseTextDocumentEmitter.fire(model);
}
protected fireDidChangeContent(event: MonacoModelContentChangedEvent): void {
this.onDidChangeTextDocumentEmitter.fire(event);
}
protected fireDidSave(model: MonacoEditorModel): void {
this.onDidSaveTextDocumentEmitter.fire(model);
}
protected readonly suppressedOpenIfDirty: MonacoEditorModel[] = [];
protected openEditorIfDirty(model: MonacoEditorModel): void {
if (model.suppressOpenEditorWhenDirty || this.suppressedOpenIfDirty.indexOf(model) !== -1) {
return;
}
if (model.dirty && MonacoEditor.findByDocument(this.editorManager, model).length === 0) {
// create a new reference to make sure the model is not disposed before it is
// acquired by the editor, thus losing the changes that made it dirty.
this.textModelService.createModelReference(model.textEditorModel.uri).then(ref => {
(
this.saveService.autoSave !== 'off' ? new Promise(resolve => model.onDidSaveModel(resolve)) :
this.editorManager.open(new URI(model.uri), { mode: 'open' })
).then(
() => ref.dispose()
);
});
}
}
protected async suppressOpenIfDirty(model: MonacoEditorModel, cb: () => MaybePromise<void>): Promise<void> {
this.suppressedOpenIfDirty.push(model);
try {
await cb();
} finally {
const i = this.suppressedOpenIfDirty.indexOf(model);
if (i !== -1) {
this.suppressedOpenIfDirty.splice(i, 1);
}
}
}
/**
* Applies given edits to the given model.
* The model is saved if no editors is opened for it.
*/
applyBackgroundEdit(model: MonacoEditorModel, editOperations: monaco.editor.IIdentifiedSingleEditOperation[],
shouldSave?: boolean | ((openEditor: MonacoEditor | undefined, wasDirty: boolean) => boolean)): Promise<void> {
return this.suppressOpenIfDirty(model, async () => {
const editor = MonacoEditor.findByDocument(this.editorManager, model)[0];
const wasDirty = !!editor?.document.dirty;
const cursorState = editor && editor.getControl().getSelections() || [];
model.textEditorModel.pushStackElement();
model.textEditorModel.pushEditOperations(cursorState, editOperations, () => cursorState);
model.textEditorModel.pushStackElement();
if ((typeof shouldSave === 'function' && shouldSave(editor, wasDirty)) || (!editor && shouldSave)) {
await model.save();
}
});
}
async applyBulkEdit(edits: ResourceEdit[], options?: IBulkEditOptions): Promise<IBulkEditResult> {
try {
let totalEdits = 0;
let totalFiles = 0;
const fileEdits = edits.filter(edit => edit instanceof MonacoResourceFileEdit);
const [snippetEdits, textEdits] = ArrayUtils.partition(edits.filter(edit => edit instanceof MonacoResourceTextEdit) as MonacoResourceTextEdit[],
edit => edit.textEdit.insertAsSnippet && (edit.resource.toString() === this.editorManager.activeEditor?.getResourceUri()?.toString()));
if (fileEdits.length > 0) {
await this.performFileEdits(<MonacoResourceFileEdit[]>fileEdits);
}
if (textEdits.length > 0) {
const result = await this.performTextEdits(<MonacoResourceTextEdit[]>textEdits);
totalEdits += result.totalEdits;
totalFiles += result.totalFiles;
}
if (snippetEdits.length > 0) {
await this.performSnippetEdits(<MonacoResourceTextEdit[]>snippetEdits);
}
// when enabled (option AND setting) loop over all dirty working copies and trigger save
// for those that were involved in this bulk edit operation.
const resources = new Set<string>(
edits
.filter((edit): edit is MonacoResourceTextEdit => edit instanceof MonacoResourceTextEdit)
.map(edit => edit.resource.toString())
);
if (resources.size > 0 && options?.respectAutoSaveConfig && this.editorPreferences.get('files.refactoring.autoSave') === true) {
await this.saveAll(resources);
}
const ariaSummary = this.getAriaSummary(totalEdits, totalFiles);
return { ariaSummary, isApplied: true };
} catch (e) {
console.error('Failed to apply Resource edits:', e);
return {
ariaSummary: `Error applying Resource edits: ${e.toString()}`,
isApplied: false
};
}
}
protected async saveAll(resources: Set<string>): Promise<void> {
await Promise.all(Array.from(resources.values()).map(uri => this.textModelService.get(uri)?.save()));
}
protected getAriaSummary(totalEdits: number, totalFiles: number): string {
if (totalEdits === 0) {
return nls.localizeByDefault('Made no edits');
}
if (totalEdits > 1 && totalFiles > 1) {
return nls.localizeByDefault('Made {0} text edits in {1} files', totalEdits, totalFiles);
}
return nls.localizeByDefault('Made {0} text edits in one file', totalEdits);
}
protected async performTextEdits(edits: MonacoResourceTextEdit[]): Promise<{
totalEdits: number,
totalFiles: number
}> {
let totalEdits = 0;
let totalFiles = 0;
const resourceEdits = new Map<string, MonacoResourceTextEdit[]>();
for (const edit of edits) {
if (typeof edit.versionId === 'number') {
const model = this.textModelService.get(edit.resource.toString());
if (model && model.textEditorModel.getVersionId() !== edit.versionId) {
throw new Error(`${model.uri} has changed in the meantime`);
}
}
const key = edit.resource.toString();
let array = resourceEdits.get(key);
if (!array) {
array = [];
resourceEdits.set(key, array);
}
array.push(edit);
}
const pending: Promise<void>[] = [];
for (const [key, value] of resourceEdits) {
pending.push((async () => {
const uri = monaco.Uri.parse(key);
let eol: EndOfLineSequence | undefined;
const editOperations: monaco.editor.IIdentifiedSingleEditOperation[] = [];
const minimalEdits = await StandaloneServices.get(IEditorWorkerService)
.computeMoreMinimalEdits(uri, value.map(edit => this.transformSnippetStringToInsertText(edit)));
if (minimalEdits) {
for (const textEdit of minimalEdits) {
if (typeof textEdit.eol === 'number') {
eol = textEdit.eol;
}
if (monaco.Range.isEmpty(textEdit.range) && !textEdit.text) {
// skip no-op
continue;
}
editOperations.push({
forceMoveMarkers: false,
range: monaco.Range.lift(textEdit.range),
text: textEdit.text
});
}
}
if (!editOperations.length && eol === undefined) {
return;
}
const reference = await this.textModelService.createModelReference(uri);
try {
const document = reference.object as MonacoEditorModel;
const model = document.textEditorModel;
const editor = MonacoEditor.findByDocument(this.editorManager, document)[0];
const cursorState = editor?.getControl().getSelections() ?? [];
// start a fresh operation
model.pushStackElement();
if (editOperations.length) {
model.pushEditOperations(cursorState, editOperations, () => cursorState);
}
if (eol !== undefined) {
model.pushEOL(eol);
}
// push again to make this change an undoable operation
model.pushStackElement();
totalFiles += 1;
totalEdits += editOperations.length;
} finally {
reference.dispose();
}
})());
}
await Promise.all(pending);
return { totalEdits, totalFiles };
}
protected async performFileEdits(edits: MonacoResourceFileEdit[]): Promise<void> {
for (const edit of edits) {
const options = edit.options || {};
if (edit.newResource && edit.oldResource) {
// rename
if (options.overwrite === undefined && options.ignoreIfExists && await this.fileService.exists(URI.fromComponents(edit.newResource))) {
return; // not overwriting, but ignoring, and the target file exists
}
await this.fileService.move(URI.fromComponents(edit.oldResource), URI.fromComponents(edit.newResource), { overwrite: options.overwrite });
} else if (!edit.newResource && edit.oldResource) {
// delete file
if (await this.fileService.exists(URI.fromComponents(edit.oldResource))) {
let useTrash = this.filePreferences['files.enableTrash'];
if (useTrash && !(this.fileService.hasCapability(URI.fromComponents(edit.oldResource), FileSystemProviderCapabilities.Trash))) {
useTrash = false; // not supported by provider
}
await this.fileService.delete(URI.fromComponents(edit.oldResource), { useTrash, recursive: options.recursive });
} else if (!options.ignoreIfNotExists) {
throw new Error(`${edit.oldResource} does not exist and can not be deleted`);
}
} else if (edit.newResource && !edit.oldResource) {
// create file
if (options.overwrite === undefined && options.ignoreIfExists && await this.fileService.exists(URI.fromComponents(edit.newResource))) {
return; // not overwriting, but ignoring, and the target file exists
}
await this.fileService.create(URI.fromComponents(edit.newResource), undefined, { overwrite: options.overwrite });
}
}
}
protected async performSnippetEdits(edits: MonacoResourceTextEdit[]): Promise<void> {
const activeEditor = MonacoEditor.getActive(this.editorManager)?.getControl();
if (activeEditor) {
const snippetController: SnippetController2 = activeEditor.getContribution('snippetController2')!;
snippetController.apply(edits.map(edit => ({ range: monaco.Range.lift(edit.textEdit.range), template: edit.textEdit.text })));
}
}
protected transformSnippetStringToInsertText(resourceEdit: MonacoResourceTextEdit): TextEdit & { insertAsSnippet?: boolean } {
if (resourceEdit.textEdit.insertAsSnippet) {
return { ...resourceEdit.textEdit, insertAsSnippet: false, text: SnippetParser.asInsertText(resourceEdit.textEdit.text) };
} else {
return resourceEdit.textEdit;
}
}
}

View File

@@ -0,0 +1,158 @@
// *****************************************************************************
// Copyright (C) 2020 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { Position, Range, Location, TextEdit, Diagnostic, DiagnosticRelatedInformation } from '@theia/core/shared/vscode-languageserver-protocol';
import { RecursivePartial } from '@theia/core/lib/common/types';
import * as monaco from '@theia/monaco-editor-core';
@injectable()
export class ProtocolToMonacoConverter {
asRange(range: undefined): undefined;
asRange(range: Range): monaco.Range;
asRange(range: Range | undefined): monaco.Range | undefined;
asRange(range: RecursivePartial<Range>): Partial<monaco.IRange>;
asRange(range: RecursivePartial<Range> | undefined): monaco.Range | Partial<monaco.IRange> | undefined;
asRange(range: RecursivePartial<Range> | undefined): monaco.Range | Partial<monaco.IRange> | undefined {
if (range === undefined) {
return undefined;
}
const start = this.asPosition(range.start);
const end = this.asPosition(range.end);
if (start instanceof monaco.Position && end instanceof monaco.Position) {
return new monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column);
}
const startLineNumber = !start || start.lineNumber === undefined ? undefined : start.lineNumber;
const startColumn = !start || start.column === undefined ? undefined : start.column;
const endLineNumber = !end || end.lineNumber === undefined ? undefined : end.lineNumber;
const endColumn = !end || end.column === undefined ? undefined : end.column;
return { startLineNumber, startColumn, endLineNumber, endColumn };
}
asPosition(position: undefined): undefined;
asPosition(position: Position): monaco.Position;
asPosition(position: Position | undefined): monaco.Position | undefined;
asPosition(position: Partial<Position>): Partial<monaco.IPosition>;
asPosition(position: Partial<Position> | undefined): monaco.Position | Partial<monaco.IPosition> | undefined;
asPosition(position: Partial<Position> | undefined): monaco.Position | Partial<monaco.IPosition> | undefined {
if (position === undefined) {
return undefined;
}
const { line, character } = position;
const lineNumber = line === undefined ? undefined : line + 1;
const column = character === undefined ? undefined : character + 1;
if (lineNumber !== undefined && column !== undefined) {
return new monaco.Position(lineNumber, column);
}
return { lineNumber, column };
}
asLocation(item: Location): monaco.languages.Location;
asLocation(item: undefined): undefined;
asLocation(item: Location | undefined): monaco.languages.Location | undefined;
asLocation(item: Location | undefined): monaco.languages.Location | undefined {
if (!item) {
return undefined;
}
const uri = monaco.Uri.parse(item.uri);
const range = this.asRange(item.range)!;
return {
uri, range
};
}
asTextEdit(edit: TextEdit): monaco.languages.TextEdit;
asTextEdit(edit: undefined): undefined;
asTextEdit(edit: TextEdit | undefined): undefined;
asTextEdit(edit: TextEdit | undefined): monaco.languages.TextEdit | undefined {
if (!edit) {
return undefined;
}
const range = this.asRange(edit.range)!;
return {
range,
text: edit.newText
};
}
asTextEdits(items: TextEdit[]): monaco.languages.TextEdit[];
asTextEdits(items: undefined): undefined;
asTextEdits(items: TextEdit[] | undefined): monaco.languages.TextEdit[] | undefined;
asTextEdits(items: TextEdit[] | undefined): monaco.languages.TextEdit[] | undefined {
if (!items) {
return undefined;
}
return items.map(item => this.asTextEdit(item));
}
asSeverity(severity?: number): monaco.MarkerSeverity {
if (severity === 1) {
return monaco.MarkerSeverity.Error;
}
if (severity === 2) {
return monaco.MarkerSeverity.Warning;
}
if (severity === 3) {
return monaco.MarkerSeverity.Info;
}
return monaco.MarkerSeverity.Hint;
}
asDiagnostics(diagnostics: undefined): undefined;
asDiagnostics(diagnostics: Diagnostic[]): monaco.editor.IMarkerData[];
asDiagnostics(diagnostics: Diagnostic[] | undefined): monaco.editor.IMarkerData[] | undefined;
asDiagnostics(diagnostics: Diagnostic[] | undefined): monaco.editor.IMarkerData[] | undefined {
if (!diagnostics) {
return undefined;
}
return diagnostics.map(diagnostic => this.asDiagnostic(diagnostic));
}
asDiagnostic(diagnostic: Diagnostic): monaco.editor.IMarkerData {
return {
code: typeof diagnostic.code === 'number' ? diagnostic.code.toString() : diagnostic.code,
severity: this.asSeverity(diagnostic.severity),
message: diagnostic.message,
source: diagnostic.source,
startLineNumber: diagnostic.range.start.line + 1,
startColumn: diagnostic.range.start.character + 1,
endLineNumber: diagnostic.range.end.line + 1,
endColumn: diagnostic.range.end.character + 1,
relatedInformation: this.asRelatedInformations(diagnostic.relatedInformation),
tags: diagnostic.tags
};
}
asRelatedInformations(relatedInformation?: DiagnosticRelatedInformation[]): monaco.editor.IRelatedInformation[] | undefined {
if (!relatedInformation) {
return undefined;
}
return relatedInformation.map(item => this.asRelatedInformation(item));
}
asRelatedInformation(relatedInformation: DiagnosticRelatedInformation): monaco.editor.IRelatedInformation {
return {
resource: monaco.Uri.parse(relatedInformation.location.uri),
startLineNumber: relatedInformation.location.range.start.line + 1,
startColumn: relatedInformation.location.range.start.character + 1,
endLineNumber: relatedInformation.location.range.end.line + 1,
endColumn: relatedInformation.location.range.end.character + 1,
message: relatedInformation.message
};
}
}

View File

@@ -0,0 +1,234 @@
// *****************************************************************************
// Copyright (C) 2023 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { EditorServiceOverrides, MonacoEditor, MonacoEditorServices } from './monaco-editor';
import { CodeEditorWidget, ICodeEditorWidgetOptions } from '@theia/monaco-editor-core/esm/vs/editor/browser/widget/codeEditor/codeEditorWidget';
import { IInstantiationService } from '@theia/monaco-editor-core/esm/vs/platform/instantiation/common/instantiation';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
import { ServiceCollection } from '@theia/monaco-editor-core/esm/vs/platform/instantiation/common/serviceCollection';
import { Disposable, DisposableCollection, Emitter, Event, TextDocumentContentChangeDelta, URI } from '@theia/core';
import { MonacoEditorModel } from './monaco-editor-model';
import { Dimension, EditorMouseEvent, MouseTarget, Position, TextDocumentChangeEvent } from '@theia/editor/lib/browser';
import * as monaco from '@theia/monaco-editor-core';
import { ElementExt } from '@theia/core/shared/@lumino/domutils';
import { Selection } from '@theia/editor/lib/browser/editor';
import { SelectionDirection } from '@theia/monaco-editor-core/esm/vs/editor/common/core/selection';
import { ShowLightbulbIconMode } from '@theia/monaco-editor-core/esm/vs/editor/common/config/editorOptions';
export class SimpleMonacoEditor extends MonacoEditorServices implements Disposable {
protected editor: CodeEditorWidget;
protected readonly toDispose = new DisposableCollection();
protected readonly onCursorPositionChangedEmitter = new Emitter<Position>();
protected readonly onFocusChangedEmitter = new Emitter<boolean>();
protected readonly onDocumentContentChangedEmitter = new Emitter<TextDocumentChangeEvent>();
readonly onDocumentContentChanged = this.onDocumentContentChangedEmitter.event;
protected readonly onMouseDownEmitter = new Emitter<EditorMouseEvent>();
readonly onDidChangeReadOnly = this.document.onDidChangeReadOnly;
protected readonly onLanguageChangedEmitter = new Emitter<string>();
readonly onLanguageChanged = this.onLanguageChangedEmitter.event;
protected readonly onScrollChangedEmitter = new Emitter<void>();
readonly onEncodingChanged = this.document.onDidChangeEncoding;
protected readonly onResizeEmitter = new Emitter<Dimension | null>();
readonly onDidResize = this.onResizeEmitter.event;
get onDispose(): Event<void> {
return this.editor.onDidDispose;
}
constructor(
readonly uri: URI,
readonly document: MonacoEditorModel,
readonly node: HTMLElement,
services: MonacoEditorServices,
options?: MonacoEditor.IOptions,
override?: EditorServiceOverrides,
widgetOptions?: ICodeEditorWidgetOptions
) {
super(services);
this.toDispose.pushAll([
this.onCursorPositionChangedEmitter,
this.onFocusChangedEmitter,
this.onDocumentContentChangedEmitter,
this.onMouseDownEmitter,
this.onLanguageChangedEmitter,
this.onScrollChangedEmitter
]);
this.toDispose.push(this.create({
...MonacoEditor.createReadOnlyOptions(document.readOnly),
...options,
model: undefined,
}, override, widgetOptions));
this.addHandlers(this.editor);
this.editor.setModel(document.textEditorModel);
}
getControl(): CodeEditorWidget {
return this.editor;
}
onSelectionChanged(listener: (range: Selection) => void): Disposable {
return this.editor.onDidChangeCursorSelection(event =>
listener({
...this.m2p.asRange(event.selection),
direction: event.selection.getDirection() === SelectionDirection.LTR ? 'ltr' : 'rtl'
}));
}
protected create(options?: MonacoEditor.IOptions, override?: EditorServiceOverrides, widgetOptions?: ICodeEditorWidgetOptions): Disposable {
const combinedOptions = {
...options,
lightbulb: { enabled: ShowLightbulbIconMode.On },
fixedOverflowWidgets: true,
automaticLayout: true,
scrollbar: {
useShadows: false,
verticalHasArrows: false,
horizontalHasArrows: false,
verticalScrollbarSize: 10,
horizontalScrollbarSize: 10,
...options?.scrollbar,
}
};
const instantiator = this.getInstantiatorWithOverrides(override);
return this.editor = instantiator.createInstance(CodeEditorWidget, this.node, {
...combinedOptions,
dimension: {
width: 0,
height: 0
},
}, widgetOptions ?? {});
}
protected addHandlers(codeEditor: CodeEditorWidget): void {
this.toDispose.push(codeEditor.onDidChangeModelLanguage(e =>
this.fireLanguageChanged(e.newLanguage)
));
this.toDispose.push(codeEditor.onDidChangeConfiguration(() => this.refresh()));
this.toDispose.push(codeEditor.onDidChangeModel(() => this.refresh()));
this.toDispose.push(codeEditor.onDidChangeModelContent(e => {
this.refresh();
this.onDocumentContentChangedEmitter.fire({ document: this.document, contentChanges: e.changes.map(this.mapModelContentChange.bind(this)) });
}));
this.toDispose.push(codeEditor.onMouseDown(e => {
const { element, position, range } = e.target;
this.onMouseDownEmitter.fire({
target: {
...(e.target as unknown as MouseTarget),
element: element || undefined,
mouseColumn: this.m2p.asPosition(undefined, e.target.mouseColumn).character,
range: range && this.m2p.asRange(range) || undefined,
position: position && this.m2p.asPosition(position.lineNumber, position.column) || undefined,
detail: undefined
},
event: e.event.browserEvent
});
}));
this.toDispose.push(codeEditor.onDidScrollChange(e => {
this.onScrollChangedEmitter.fire(undefined);
}));
this.toDispose.push(this.onDidChangeReadOnly(readOnly => {
codeEditor.updateOptions(MonacoEditor.createReadOnlyOptions(readOnly));
}));
}
setLanguage(languageId: string): void {
monaco.editor.setModelLanguage(this.document.textEditorModel, languageId);
}
protected fireLanguageChanged(languageId: string): void {
this.onLanguageChangedEmitter.fire(languageId);
}
protected getInstantiatorWithOverrides(override?: EditorServiceOverrides): IInstantiationService {
const instantiator = StandaloneServices.get(IInstantiationService);
if (override) {
const overrideServices = new ServiceCollection(...override);
const childService = instantiator.createChild(overrideServices);
this.toDispose.push(childService);
return childService;
}
return instantiator;
}
protected mapModelContentChange(change: monaco.editor.IModelContentChange): TextDocumentContentChangeDelta {
return {
range: this.m2p.asRange(change.range),
rangeLength: change.rangeLength,
text: change.text
};
}
focus(): void {
this.editor.focus();
}
refresh(): void {
this.autoresize();
}
resizeToFit(): void {
this.autoresize();
// eslint-disable-next-line no-null/no-null
this.onResizeEmitter.fire(null);
}
setSize(dimension: Dimension): void {
this.resize(dimension);
this.onResizeEmitter.fire(dimension);
}
protected autoresize(): void {
this.resize();
}
protected resize(dimension?: Dimension): void {
if (this.node) {
const layoutSize = this.computeLayoutSize(this.node, dimension);
this.editor.layout(layoutSize);
}
}
protected computeLayoutSize(hostNode: HTMLElement, dimension: monaco.editor.IDimension | undefined): monaco.editor.IDimension {
if (dimension && dimension.width >= 0 && dimension.height >= 0) {
return dimension;
}
const boxSizing = ElementExt.boxSizing(hostNode);
const width = (!dimension || dimension.width < 0) ?
this.getWidth(hostNode, boxSizing) :
dimension.width;
const height = (!dimension || dimension.height < 0) ?
this.getHeight(hostNode, boxSizing) :
dimension.height;
return { width, height };
}
protected getWidth(hostNode: HTMLElement, boxSizing: ElementExt.IBoxSizing): number {
return hostNode.offsetWidth - boxSizing.horizontalSum;
}
protected getHeight(hostNode: HTMLElement, boxSizing: ElementExt.IBoxSizing): number {
return this.editor.getContentHeight();
}
dispose(): void {
this.toDispose.dispose();
}
}

View File

@@ -0,0 +1,296 @@
.monaco-editor {
padding-bottom: 5.6px;
font-family: var(--theia-code-font-family);
font-size: inherit !important;
}
#quick-input-container {
position: fixed;
right: 50%;
z-index: 1000000;
}
/*
* set z-index to 0, so tabs are not above overlay widgets
*/
.lm-TabBar.theia-app-centers {
z-index: 0;
display: flex;
}
.monaco-editor .zone-widget {
position: absolute;
z-index: 10;
}
.monaco-editor .zone-widget .zone-widget-container {
border-top-style: solid;
border-bottom-style: solid;
border-top-width: 0;
border-bottom-width: 0;
border-top-color: var(--theia-peekView-border);
border-bottom-color: var(--theia-peekView-border);
position: relative;
}
.monaco-editor .parameter-hints-widget > .wrapper {
overflow: hidden;
}
/* List highlight, see https://github.com/microsoft/vscode/blob/ff5f581425da6230b6f9216ecf19abf6c9d285a6/src/vs/workbench/browser/style.ts#L50 */
.monaco-tree .monaco-tree-row .monaco-highlighted-label .highlight,
.monaco-list .monaco-list-row .monaco-highlighted-label .highlight {
color: var(--theia-list-highlightForeground) !important;
}
/* Scrollbars, see https://github.com/microsoft/vscode/blob/ff5f581425da6230b6f9216ecf19abf6c9d285a6/src/vs/workbench/browser/style.ts#L65 */
.monaco-scrollable-element > .shadow.top {
box-shadow: var(--theia-scrollbar-shadow) 0 6px 6px -6px inset !important;
}
.monaco-scrollable-element > .shadow.left {
box-shadow: var(--theia-scrollbar-shadow) 6px 0 6px -6px inset !important;
}
.monaco-scrollable-element > .shadow.top.left {
box-shadow: var(--theia-scrollbar-shadow) 6px 6px 6px -6px inset !important;
}
.monaco-scrollable-element > .scrollbar > .slider {
background: var(--theia-scrollbarSlider-background) !important;
}
.monaco-scrollable-element > .scrollbar > .slider:hover {
background: var(--theia-scrollbarSlider-hoverBackground) !important;
}
.monaco-scrollable-element > .scrollbar > .slider.active {
background: var(--theia-scrollbarSlider-activeBackground) !important;
}
.monaco-editor .codicon.codicon-debug-start {
color: var(--theia-debugIcon-startForeground) !important;
}
.monaco-editor .codelens-decoration a {
color: inherit !important;
}
.monaco-editor .reference-zone-widget .ref-tree .referenceMatch .highlight {
color: unset !important;
}
.monaco-editor .find-widget .monaco-inputbox.synthetic-focus {
outline: var(--theia-border-width) solid;
outline-offset: calc(-1 * var(--theia-border-width));
outline-color: var(--theia-focusBorder);
}
.monaco-editor .rename-box input {
color: var(--theia-editor-foreground);
}
.monaco-editor .rename-box .rename-label {
opacity: 0.8;
padding: 3px;
font-family: sans-serif;
}
/* Monaco Quick Input */
.quick-input-widget {
background-color: var(--theia-quickInput-background) !important;
color: var(--theia-foreground) !important;
}
.quick-input-list .monaco-list-row.focused {
background-color: var(--theia-quickInputList-focusBackground) !important;
}
.quick-input-list .monaco-keybinding > .monaco-keybinding-key {
color: inherit !important;
}
.quick-input-list .monaco-list-row.focused,
.quick-input-list .monaco-list-row.focused .monaco-highlighted-label,
.quick-input-list .monaco-list-row.focused .monaco-highlighted-label .codicon,
.quick-input-list .monaco-list-row.focused .quick-input-list-entry .quick-input-list-separator,
.quick-input-list .monaco-list-row.focused .monaco-highlighted-label .monaco-keybinding .monaco-keybinding-key,
.quick-input-list .monaco-list-row.focused .monaco-highlighted-label .monaco-keybinding .monaco-keybinding-key-separator {
color: var(--theia-quickInputList-focusForeground) !important;
}
.quick-input-list .monaco-list-row .codicon {
color: var(--theia-foreground) !important;
}
.quick-input-list .monaco-list-row.focused .codicon {
color: var(--theia-list-foreground) !important;
}
.quick-input-list .monaco-list-row.focused .monaco-highlighted-label .highlight {
color: var(--theia-list-focusHighlightForeground) !important;
}
.quick-input-titlebar .action-item .action-label {
color: var(--theia-foreground) !important;
}
.monaco-list-rows
.monaco-list-row:not(:first-child)
.quick-input-list-entry.quick-input-list-separator-border {
border-top: 1px solid var(--theia-pickerGroup-border) !important;
}
.quick-input-list .quick-input-list-separator {
color: var(--theia-pickerGroup-foreground) !important;
}
.monaco-icon-label > .monaco-icon-label-container {
flex: 1;
}
.quick-input-list-rows
.quick-input-list-row
.monaco-icon-label
.monaco-icon-description-container
.label-description {
font-family: var(--theia-ui-font-family);
font-size: calc(var(--theia-ui-font-size1) * 0.9) !important;
color: var(--theia-foreground) !important;
white-space: inherit;
}
.quick-input-list-rows
.quick-input-list-row
.monaco-icon-label
.monaco-icon-label-container
.monaco-icon-name-container
.label-name {
font-family: var(--theia-ui-font-family);
font-size: var(--theia-ui-font-size1) !important;
color: var(--theia-foreground) !important;
}
.quick-input-list .monaco-icon-label.codicon,
.quick-input-list .monaco-icon-label.file-icon {
display: flex;
text-align: left;
}
.quick-input-list .monaco-icon-label::before {
height: 22px;
}
.codicon-file.default-file-icon.file-icon {
padding-left: 2px;
height: 22px;
}
.codicon-file.default-file-icon.file-icon::before {
margin-right: 4px;
font-size: var(--theia-ui-font-size1);
}
.quick-input-list .monaco-icon-label.codicon::before {
position: relative;
top: 3px;
}
.quick-input-list .monaco-icon-label.theia-file-icons-js {
line-height: inherit;
}
.quick-input-list .quick-input-list-label {
cursor: pointer !important;
}
.quick-input-progress.active.infinite {
background-color: var(--theia-progressBar-background);
width: 3%;
/* `progress-animation` is defined in `packages/core/src/browser/style/progress-bar.css` */
animation: progress-animation 1.3s 0s infinite cubic-bezier(0.645, 0.045, 0.355, 1);
}
.monaco-list:not(.drop-target) .monaco-list-row:hover:not(.selected):not(.focused) {
background: var(--theia-list-hoverBackground);
}
.monaco-editor .peekview-widget .head .peekview-title {
font-family: var(--theia-ui-font-family);
}
.monaco-editor .peekview-widget .referenceMatch {
font-family: var(--theia-ui-font-family);
}
.monaco-editor .find-widget .monaco-findInput .input,
.monaco-editor .find-widget .matchesCount {
font-family: var(--theia-ui-font-family);
}
.monaco-editor .monaco-action-bar .action-label.codicon {
color: var(--theia-foreground);
}
.monaco-editor .monaco-action-bar .action-item.active {
transform: none;
}
.monaco-editor .peekview-widget .monaco-list-row.focused.selected .label-name,
.monaco-editor .peekview-widget .monaco-list-row.focused.selected .label-description,
.monaco-editor .peekview-widget .monaco-list-row.focused.selected .monaco-highlighted-label,
.monaco-editor .peekview-widget .monaco-list-row.focused.selected .codicon {
color: var(--theia-list-activeSelectionForeground) !important;
}
.quick-input-titlebar {
background-color: var(--theia-quickInputTitle-background);
}
.quick-input-widget input {
background-color: var(--theia-input-background) !important;
}
.monaco-editor .suggest-widget .monaco-list .monaco-list-row.focused>.contents>.main .monaco-highlighted-label .highlight {
color: var(--vscode-editorSuggestWidget-focusHighlightForeground) !important;
}
.symbol-icon-center {
align-self: center;
margin-right: 4px;
}
.monaco-inputbox.error > .ibwrapper > .input,
.monaco-inputbox.warning > .ibwrapper > .input,
.monaco-inputbox.info > .ibwrapper > .input {
outline: 0 !important;
}
.monaco-inputbox > .ibwrapper > .input {
min-height: 28px;
border-radius: 2px;
}
.quick-input-header > .quick-input-inline-action-bar {
display: none;
}
.monaco-editor .overlay-button {
padding: 6px 11px;
border-radius: 2px;
cursor: pointer;
user-select: none;
z-index: 1;
background-color: var(--theia-button-background);
color: var(--theia-button-foreground);
border: 1px solid var(--theia-contrast-border);
font-family: var(--theia-ui-font-family);
line-height: 1.4;
}
.monaco-editor .overlay-button.theia-mod-disabled {
display: none;
}
.monaco-diff-editor .gutter .action-item .action-label {
color: var(--theia-editorGutter-itemGlyphForeground);
}

View File

@@ -0,0 +1,20 @@
// *****************************************************************************
// Copyright (C) 2018 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
export * from './textmate-registry';
export * from './textmate-contribution';
export * from './monaco-textmate-service';
export * from './monaco-textmate-frontend-bindings';

View File

@@ -0,0 +1,90 @@
// *****************************************************************************
// Copyright (C) 2018 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { interfaces } from '@theia/core/shared/inversify';
import { FrontendApplicationContribution, isBasicWasmSupported } from '@theia/core/lib/browser';
import { bindContributionProvider } from '@theia/core';
import { TextmateRegistry } from './textmate-registry';
import { LanguageGrammarDefinitionContribution } from './textmate-contribution';
import { MonacoTextmateService } from './monaco-textmate-service';
import { MonacoThemeRegistry } from './monaco-theme-registry';
import { loadWASM, createOnigScanner, OnigScanner, createOnigString, OnigString } from 'vscode-oniguruma';
import { IOnigLib, IRawGrammar, parseRawGrammar, Registry } from 'vscode-textmate';
import { OnigasmProvider, TextmateRegistryFactory, ThemeMix } from './monaco-theme-types';
export class OnigasmLib implements IOnigLib {
createOnigScanner(sources: string[]): OnigScanner {
return createOnigScanner(sources);
}
createOnigString(sources: string): OnigString {
return createOnigString(sources);
}
}
export default (bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind) => {
const onigLib = createOnigasmLib();
bind(OnigasmProvider).toConstantValue(() => onigLib);
bind(MonacoTextmateService).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(MonacoTextmateService);
bindContributionProvider(bind, LanguageGrammarDefinitionContribution);
bind(TextmateRegistry).toSelf().inSingletonScope();
bind(MonacoThemeRegistry).toSelf().inSingletonScope();
bind(TextmateRegistryFactory).toFactory(({ container }) => (theme?: ThemeMix) => {
const onigProvider = container.get<OnigasmProvider>(OnigasmProvider);
const textmateRegistry = container.get(TextmateRegistry);
return new Registry({
onigLib: onigProvider(),
theme,
loadGrammar: async (scopeName: string) => {
const provider = textmateRegistry.getProvider(scopeName);
if (provider) {
const definition = await provider.getGrammarDefinition();
let rawGrammar: IRawGrammar;
if (typeof definition.content === 'string') {
rawGrammar = parseRawGrammar(definition.content, definition.format === 'json' ? 'grammar.json' : 'grammar.plist');
} else {
rawGrammar = definition.content as IRawGrammar;
}
return rawGrammar;
}
return undefined;
},
getInjections: (scopeName: string) => {
const provider = textmateRegistry.getProvider(scopeName);
if (provider && provider.getInjections) {
return provider.getInjections(scopeName);
}
return [];
}
});
});
};
export async function createOnigasmLib(): Promise<IOnigLib> {
if (!isBasicWasmSupported) {
throw new Error('wasm not supported');
}
const wasm = await fetchOnigasm();
await loadWASM(wasm);
return new OnigasmLib();
}
export async function fetchOnigasm(): Promise<ArrayBuffer> {
// Using Webpack's wasm loader should give us a URL to fetch the resource from:
const onigasmPath: string = require('vscode-oniguruma/release/onig.wasm');
const response = await fetch(onigasmPath, { method: 'GET' });
return response.arrayBuffer();
}

View File

@@ -0,0 +1,187 @@
// *****************************************************************************
// Copyright (C) 2018 Redhat, Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject, named } from '@theia/core/shared/inversify';
import { Registry } from 'vscode-textmate';
import { ILogger, ContributionProvider, DisposableCollection, Disposable } from '@theia/core';
import { FrontendApplicationContribution, isBasicWasmSupported } from '@theia/core/lib/browser';
import { ThemeService } from '@theia/core/lib/browser/theming';
import { LanguageGrammarDefinitionContribution, getEncodedLanguageId } from './textmate-contribution';
import { createTextmateTokenizer, TokenizerOption } from './textmate-tokenizer';
import { TextmateRegistry } from './textmate-registry';
import { MonacoThemeRegistry } from './monaco-theme-registry';
import { EditorPreferences } from '@theia/editor/lib/common/editor-preferences';
import * as monaco from '@theia/monaco-editor-core';
import { TokenizationRegistry } from '@theia/monaco-editor-core/esm/vs/editor/common/languages';
import { IStandaloneThemeService } from '@theia/monaco-editor-core/esm/vs/editor/standalone/common/standaloneTheme';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
import { ILanguageService } from '@theia/monaco-editor-core/esm/vs/editor/common/languages/language';
import { TokenizationSupportAdapter } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneLanguages';
import { LanguageService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/languageService';
import { OnigasmProvider, TextmateRegistryFactory } from './monaco-theme-types';
@injectable()
export class MonacoTextmateService implements FrontendApplicationContribution {
protected readonly tokenizerOption: TokenizerOption = {
lineLimit: 400
};
protected readonly _activatedLanguages = new Set<string>();
protected grammarRegistry: Registry;
@inject(ContributionProvider) @named(LanguageGrammarDefinitionContribution)
protected readonly grammarProviders: ContributionProvider<LanguageGrammarDefinitionContribution>;
@inject(TextmateRegistry)
protected readonly textmateRegistry: TextmateRegistry;
@inject(ILogger)
protected readonly logger: ILogger;
@inject(OnigasmProvider)
protected readonly onigasmProvider: OnigasmProvider;
@inject(ThemeService)
protected readonly themeService: ThemeService;
@inject(MonacoThemeRegistry)
protected readonly monacoThemeRegistry: MonacoThemeRegistry;
@inject(EditorPreferences)
protected readonly preferences: EditorPreferences;
@inject(TextmateRegistryFactory)
protected readonly registryFactory: TextmateRegistryFactory;
initialize(): void {
if (!isBasicWasmSupported) {
console.log('Textmate support deactivated because WebAssembly is not detected.');
return;
}
for (const grammarProvider of this.grammarProviders.getContributions()) {
try {
grammarProvider.registerTextmateLanguage(this.textmateRegistry);
} catch (err) {
console.error(err);
}
}
this.grammarRegistry = this.registryFactory(this.monacoThemeRegistry.getThemeData(this.currentEditorTheme));
this.tokenizerOption.lineLimit = this.preferences['editor.maxTokenizationLineLength'];
this.preferences.onPreferenceChanged(e => {
if (e.preferenceName === 'editor.maxTokenizationLineLength') {
this.tokenizerOption.lineLimit = this.preferences['editor.maxTokenizationLineLength'];
}
});
this.updateTheme();
this.themeService.onDidColorThemeChange(() => this.updateTheme());
for (const id of this.textmateRegistry.languages) {
this.activateLanguage(id);
}
}
protected readonly toDisposeOnUpdateTheme = new DisposableCollection();
protected updateTheme(): void {
this.toDisposeOnUpdateTheme.dispose();
const currentEditorTheme = this.currentEditorTheme;
document.body.classList.add(currentEditorTheme);
this.toDisposeOnUpdateTheme.push(Disposable.create(() => document.body.classList.remove(currentEditorTheme)));
// first update registry to run tokenization with the proper theme
const theme = this.monacoThemeRegistry.getThemeData(currentEditorTheme);
if (theme) {
this.grammarRegistry.setTheme(theme);
}
// then trigger tokenization by setting monaco theme
monaco.editor.setTheme(currentEditorTheme);
}
protected get currentEditorTheme(): string {
return this.themeService.getCurrentTheme().editorTheme || MonacoThemeRegistry.DARK_DEFAULT_THEME;
}
activateLanguage(language: string): Disposable {
const toDispose = new DisposableCollection(
Disposable.create(() => { /* mark as not disposed */ })
);
toDispose.push(this.waitForLanguage(language, () =>
this.doActivateLanguage(language, toDispose)
));
return toDispose;
}
protected async doActivateLanguage(languageId: string, toDispose: DisposableCollection): Promise<void> {
if (this._activatedLanguages.has(languageId)) {
return;
}
this._activatedLanguages.add(languageId);
toDispose.push(Disposable.create(() => this._activatedLanguages.delete(languageId)));
const scopeName = this.textmateRegistry.getScope(languageId);
if (!scopeName) {
return;
}
const provider = this.textmateRegistry.getProvider(scopeName);
if (!provider) {
return;
}
const configuration = this.textmateRegistry.getGrammarConfiguration(languageId);
const initialLanguage = getEncodedLanguageId(languageId);
await this.onigasmProvider();
if (toDispose.disposed) {
return;
}
try {
const grammar = await this.grammarRegistry.loadGrammarWithConfiguration(scopeName, initialLanguage, configuration);
if (toDispose.disposed) {
return;
}
if (!grammar) {
throw new Error(`no grammar for ${scopeName}, ${initialLanguage}, ${JSON.stringify(configuration)}`);
}
const options = configuration.tokenizerOption ? configuration.tokenizerOption : this.tokenizerOption;
const tokenizer = createTextmateTokenizer(grammar, options);
toDispose.push(monaco.languages.setTokensProvider(languageId, tokenizer));
const support = TokenizationRegistry.get(languageId);
const themeService = StandaloneServices.get(IStandaloneThemeService);
const languageService = StandaloneServices.get(ILanguageService);
const adapter = new TokenizationSupportAdapter(languageId, tokenizer, languageService, themeService);
support!.tokenize = adapter.tokenize.bind(adapter);
} catch (error) {
this.logger.warn('No grammar for this language id', languageId, error);
}
}
protected waitForLanguage(language: string, cb: () => {}): Disposable {
const languageService = StandaloneServices.get(ILanguageService) as LanguageService;
if (languageService['_requestedBasicLanguages'].has(language)) {
cb();
return Disposable.NULL;
}
return monaco.languages.onLanguage(language, cb);
}
}

View File

@@ -0,0 +1,176 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
/* eslint-disable @typescript-eslint/no-explicit-any */
import { inject, injectable } from '@theia/core/shared/inversify';
import { IRawTheme } from 'vscode-textmate';
import * as monaco from '@theia/monaco-editor-core';
import { IStandaloneThemeService } from '@theia/monaco-editor-core/esm/vs/editor/standalone/common/standaloneTheme';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
import { StandaloneThemeService } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneThemeService';
import { Color } from '@theia/monaco-editor-core/esm/vs/base/common/color';
import { MixStandaloneTheme, TextmateRegistryFactory, ThemeMix } from './monaco-theme-types';
@injectable()
export class MonacoThemeRegistry {
@inject(TextmateRegistryFactory) protected readonly registryFactory: TextmateRegistryFactory;
initializeDefaultThemes(): void {
this.register(require('../../../data/monaco-themes/vscode/dark_theia.json'), {
'./dark_vs.json': require('../../../data/monaco-themes/vscode/dark_vs.json'),
'./dark_plus.json': require('../../../data/monaco-themes/vscode/dark_plus.json')
}, 'dark-theia', 'vs-dark');
this.register(require('../../../data/monaco-themes/vscode/light_theia.json'), {
'./light_vs.json': require('../../../data/monaco-themes/vscode/light_vs.json'),
'./light_plus.json': require('../../../data/monaco-themes/vscode/light_plus.json'),
}, 'light-theia', 'vs');
this.register(require('../../../data/monaco-themes/vscode/hc_theia.json'), {
'./hc_black.json': require('../../../data/monaco-themes/vscode/hc_black.json')
}, 'hc-theia', 'hc-black');
this.register(require('../../../data/monaco-themes/vscode/hc_theia_light.json'), {
'./hc_light.json': require('../../../data/monaco-themes/vscode/hc_light.json')
}, 'hc-theia-light', 'hc-light');
}
getThemeData(): ThemeMix;
getThemeData(name: string): ThemeMix | undefined;
getThemeData(name?: string): ThemeMix | undefined {
const theme = this.doGetTheme(name);
return theme && theme.themeData;
}
getTheme(): MixStandaloneTheme;
getTheme(name: string): MixStandaloneTheme | undefined;
getTheme(name?: string): MixStandaloneTheme | undefined {
return this.doGetTheme(name);
}
protected doGetTheme(name: string | undefined): MixStandaloneTheme | undefined {
const standaloneThemeService = StandaloneServices.get(IStandaloneThemeService) as StandaloneThemeService;
const theme = !name ? standaloneThemeService.getColorTheme() : standaloneThemeService['_knownThemes'].get(name);
return theme as MixStandaloneTheme | undefined;
}
setTheme(name: string, data: ThemeMix): void {
// monaco auto refreshes a theme with new data
monaco.editor.defineTheme(name, data);
}
/**
* Register VS Code compatible themes
*/
register(json: any, includes?: { [includePath: string]: any }, givenName?: string, monacoBase?: monaco.editor.BuiltinTheme): ThemeMix {
const name = givenName || json.name!;
const result: ThemeMix = {
name,
base: monacoBase || 'vs',
inherit: true,
colors: {},
rules: [],
settings: []
};
if (typeof json.include !== 'undefined') {
if (!includes || !includes[json.include]) {
console.error(`Couldn't resolve includes theme ${json.include}.`);
} else {
const parentTheme = this.register(includes[json.include], includes);
Object.assign(result.colors, parentTheme.colors);
result.rules.push(...parentTheme.rules);
result.settings.push(...parentTheme.settings);
}
}
const tokenColors: IRawTheme['settings'] = json.tokenColors;
if (Array.isArray(tokenColors)) {
for (const tokenColor of tokenColors) {
if (tokenColor.scope && tokenColor.settings) {
result.settings.push({
scope: tokenColor.scope,
settings: {
foreground: this.normalizeColor(tokenColor.settings.foreground),
background: this.normalizeColor(tokenColor.settings.background),
fontStyle: tokenColor.settings.fontStyle
}
});
}
}
}
if (json.colors) {
Object.assign(result.colors, json.colors);
result.encodedTokensColors = Object.keys(result.colors).map(key => result.colors[key]);
}
if (monacoBase && givenName) {
for (const setting of result.settings) {
this.transform(setting, rule => result.rules.push(rule));
}
// the default rule (scope empty) is always the first rule. Ignore all other default rules.
const defaultTheme = (StandaloneServices.get(IStandaloneThemeService) as StandaloneThemeService)['_knownThemes'].get(result.base)!;
const foreground = result.colors['editor.foreground'] || defaultTheme.getColor('editor.foreground');
const background = result.colors['editor.background'] || defaultTheme.getColor('editor.background');
result.settings.unshift({
settings: {
foreground: this.normalizeColor(foreground),
background: this.normalizeColor(background)
}
});
const reg = this.registryFactory(result);
result.encodedTokensColors = reg.getColorMap();
// index 0 has to be set to null as it is 'undefined' by default, but monaco code expects it to be null
// eslint-disable-next-line no-null/no-null
result.encodedTokensColors[0] = null!;
this.setTheme(givenName, result);
}
return result;
}
protected transform(tokenColor: any, acceptor: (rule: monaco.editor.ITokenThemeRule) => void): void {
if (typeof tokenColor.scope === 'undefined') {
tokenColor.scope = [''];
} else if (typeof tokenColor.scope === 'string') {
tokenColor.scope = tokenColor.scope.split(',').map((scope: string) => scope.trim());
}
for (const scope of tokenColor.scope) {
acceptor({
...tokenColor.settings, token: scope
});
}
}
protected normalizeColor(color: string | Color | undefined): string | undefined {
if (!color) {
return undefined;
}
const normalized = String(color).replace(/^\#/, '').slice(0, 6);
if (normalized.length < 6 || !(normalized).match(/^[0-9A-Fa-f]{6}$/)) {
// ignoring not normalized colors to avoid breaking token color indexes between monaco and vscode-textmate
console.error(`Color '${normalized}' is NOT normalized, it must have 6 positions.`);
return undefined;
}
return '#' + normalized;
}
}
export namespace MonacoThemeRegistry {
export const DARK_DEFAULT_THEME = 'dark-theia';
export const LIGHT_DEFAULT_THEME = 'light-theia';
export const HC_DEFAULT_THEME = 'hc-theia';
export const HC_LIGHT_THEME = 'hc-theia-light';
}

View File

@@ -0,0 +1,36 @@
// *****************************************************************************
// Copyright (C) 2018 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import * as monaco from '@theia/monaco-editor-core';
import { IStandaloneTheme } from '@theia/monaco-editor-core/esm/vs/editor/standalone/common/standaloneTheme';
import { IOnigLib, IRawTheme, Registry } from 'vscode-textmate';
export interface ThemeMix extends IRawTheme, monaco.editor.IStandaloneThemeData { }
export interface MixStandaloneTheme extends IStandaloneTheme {
themeData: ThemeMix
}
export const OnigasmProvider = Symbol('OnigasmProvider');
export type OnigasmProvider = () => Promise<IOnigLib>;
export const TextmateRegistryFactory = Symbol('TextmateRegistryFactory');
export type TextmateRegistryFactory = (currentTheme?: ThemeMix) => Registry;
export type MonacoThemeColor = monaco.editor.IColors;
export interface MonacoTokenRule extends monaco.editor.ITokenThemeRule { };
export type MonacoBuiltinTheme = monaco.editor.BuiltinTheme;
export interface MonacoTheme extends monaco.editor.IStandaloneThemeData {
name: string;
}

View File

@@ -0,0 +1,29 @@
// *****************************************************************************
// Copyright (C) 2018 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { TextmateRegistry } from './textmate-registry';
import * as monaco from '@theia/monaco-editor-core';
/**
* Callback for extensions to contribute language grammar definitions
*/
export const LanguageGrammarDefinitionContribution = Symbol('LanguageGrammarDefinitionContribution');
export interface LanguageGrammarDefinitionContribution {
registerTextmateLanguage(registry: TextmateRegistry): void;
}
export function getEncodedLanguageId(languageId: string): number {
return monaco.languages.getEncodedLanguageId(languageId);
}

View File

@@ -0,0 +1,129 @@
// *****************************************************************************
// Copyright (C) 2018 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { IGrammarConfiguration } from 'vscode-textmate';
import { TokenizerOption } from './textmate-tokenizer';
import { Disposable } from '@theia/core/lib/common/disposable';
export interface TextmateGrammarConfiguration extends IGrammarConfiguration {
/**
* Optional options to further refine the tokenization of the grammar.
*/
readonly tokenizerOption?: TokenizerOption;
}
export interface GrammarDefinitionProvider {
getGrammarDefinition(): Promise<GrammarDefinition>;
getInjections?(scopeName: string): string[];
}
export interface GrammarDefinition {
format: 'json' | 'plist';
content: object | string;
location?: string;
}
@injectable()
export class TextmateRegistry {
protected readonly scopeToProvider = new Map<string, GrammarDefinitionProvider[]>();
protected readonly languageToConfig = new Map<string, TextmateGrammarConfiguration[]>();
protected readonly languageIdToScope = new Map<string, string[]>();
get languages(): IterableIterator<string> {
return this.languageIdToScope.keys();
}
registerTextmateGrammarScope(scope: string, provider: GrammarDefinitionProvider): Disposable {
const providers = this.scopeToProvider.get(scope) || [];
const existingProvider = providers[0];
if (existingProvider) {
Promise.all([existingProvider.getGrammarDefinition(), provider.getGrammarDefinition()]).then(([a, b]) => {
if (a.location !== b.location || !a.location && !b.location) {
console.warn(`a registered grammar provider for '${scope}' scope is overridden`);
}
});
}
providers.unshift(provider);
this.scopeToProvider.set(scope, providers);
return Disposable.create(() => {
const index = providers.indexOf(provider);
if (index !== -1) {
providers.splice(index, 1);
}
});
}
getProvider(scope: string): GrammarDefinitionProvider | undefined {
const providers = this.scopeToProvider.get(scope);
return providers && providers[0];
}
mapLanguageIdToTextmateGrammar(languageId: string, scope: string): Disposable {
const scopes = this.languageIdToScope.get(languageId) || [];
const existingScope = scopes[0];
if (typeof existingScope === 'string') {
console.warn(`'${languageId}' language is remapped from '${existingScope}' to '${scope}' scope`);
}
scopes.unshift(scope);
this.languageIdToScope.set(languageId, scopes);
return Disposable.create(() => {
const index = scopes.indexOf(scope);
if (index !== -1) {
scopes.splice(index, 1);
}
});
}
getScope(languageId: string): string | undefined {
const scopes = this.languageIdToScope.get(languageId);
return scopes && scopes[0];
}
getLanguageId(scope: string): string | undefined {
for (const languageId of this.languageIdToScope.keys()) {
if (this.getScope(languageId) === scope) {
return languageId;
}
}
return undefined;
}
registerGrammarConfiguration(languageId: string, config: TextmateGrammarConfiguration): Disposable {
const configs = this.languageToConfig.get(languageId) || [];
const existingConfig = configs[0];
if (existingConfig) {
console.warn(`a registered grammar configuration for '${languageId}' language is overridden`);
}
configs.unshift(config);
this.languageToConfig.set(languageId, configs);
return Disposable.create(() => {
const index = configs.indexOf(config);
if (index !== -1) {
configs.splice(index, 1);
}
});
}
getGrammarConfiguration(languageId: string): TextmateGrammarConfiguration {
const configs = this.languageToConfig.get(languageId);
return configs && configs[0] || {};
}
}

View File

@@ -0,0 +1,73 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import * as monaco from '@theia/monaco-editor-core';
import { SnippetParser } from '@theia/monaco-editor-core/esm/vs/editor/contrib/snippet/browser/snippetParser';
/**
* @deprecated use MonacoSnippetSuggestProvider instead
*/
export class TextmateSnippetCompletionProvider implements monaco.languages.CompletionItemProvider {
private items: monaco.languages.CompletionItem[];
constructor(protected config: TextmateSnippets, protected mdLanguage: string = '') {
this.items = [];
for (const name of Object.keys(config)) {
const textmateSnippet = config[name];
const insertText = Array.isArray(textmateSnippet.body) ? textmateSnippet.body.join('\n') : textmateSnippet.body;
this.items.push({
label: textmateSnippet.prefix,
detail: textmateSnippet.description,
kind: monaco.languages.CompletionItemKind.Snippet,
documentation: {
value: '```' + this.mdLanguage + '\n' + this.replaceVariables(insertText) + '```'
},
insertText: insertText,
range: undefined!
});
}
}
protected replaceVariables(textmateSnippet: string): string {
return new SnippetParser().parse(textmateSnippet).toString();
}
provideCompletionItems(document: monaco.editor.ITextModel,
position: monaco.Position,
context: monaco.languages.CompletionContext,
token: monaco.CancellationToken): monaco.languages.CompletionList {
return {
suggestions: this.items
};
}
}
/**
* @deprecated use JsonSerializedSnippets & MonacoSnippetSuggestProvider instead
*/
export interface TextmateSnippets {
[name: string]: TextmateSnippet;
}
/**
* @deprecated use JsonSerializedSnippet & MonacoSnippetSuggestProvider instead
*/
export interface TextmateSnippet {
readonly prefix: string,
readonly body: string[],
readonly description: string
}

View File

@@ -0,0 +1,84 @@
// *****************************************************************************
// Copyright (C) 2018 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { INITIAL, IGrammar, StateStack } from 'vscode-textmate';
import * as monaco from '@theia/monaco-editor-core';
export class TokenizerState implements monaco.languages.IState {
constructor(
public readonly stateStack: StateStack
) { }
clone(): monaco.languages.IState {
return new TokenizerState(this.stateStack);
}
equals(other: monaco.languages.IState): boolean {
return other instanceof TokenizerState && (other === this || other.stateStack === this.stateStack);
}
}
/**
* Options for the TextMate tokenizer.
*/
export interface TokenizerOption {
/**
* Maximum line length that will be handled by the TextMate tokenizer. If the length of the actual line exceeds this
* limit, the tokenizer terminates and the tokenization of any subsequent lines might be broken.
*
* If the `lineLimit` is not defined, it means, there are no line length limits. Otherwise, it must be a positive
* integer or an error will be thrown.
*/
lineLimit?: number;
}
export function createTextmateTokenizer(grammar: IGrammar, options: TokenizerOption): monaco.languages.EncodedTokensProvider & monaco.languages.TokensProvider {
if (options.lineLimit !== undefined && (options.lineLimit <= 0 || !Number.isInteger(options.lineLimit))) {
throw new Error(`The 'lineLimit' must be a positive integer. It was ${options.lineLimit}.`);
}
return {
getInitialState: () => new TokenizerState(INITIAL),
tokenizeEncoded(line: string, state: TokenizerState): monaco.languages.IEncodedLineTokens {
if (options.lineLimit !== undefined && line.length > options.lineLimit) {
// Skip tokenizing the line if it exceeds the line limit.
return { endState: state.stateStack, tokens: new Uint32Array() };
}
const result = grammar.tokenizeLine2(line, state.stateStack, 500);
return {
endState: new TokenizerState(result.ruleStack),
tokens: result.tokens
};
},
tokenize(line: string, state: TokenizerState): monaco.languages.ILineTokens {
if (options.lineLimit !== undefined && line.length > options.lineLimit) {
// Skip tokenizing the line if it exceeds the line limit.
return { endState: state.stateStack, tokens: [] };
}
const result = grammar.tokenizeLine(line, state.stateStack, 500);
return {
endState: new TokenizerState(result.ruleStack),
tokens: result.tokens.map(t => ({
startIndex: t.startIndex,
scopes: t.scopes.reverse().join(' ')
}))
};
}
};
}

View File

@@ -0,0 +1,196 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject } from '@theia/core/shared/inversify';
import { environment } from '@theia/core/shared/@theia/application-package/lib/environment';
import { KeybindingContribution, KeybindingRegistry, OpenerService, LabelProvider } from '@theia/core/lib/browser';
import { QuickAccessContribution, QuickAccessProvider, QuickInputService, QuickAccessRegistry, QuickPicks, QuickPickItem, findMatches } from '@theia/core/lib/browser/quick-input';
import {
CommandRegistry, CommandHandler, Command, SelectionService, CancellationToken,
CommandContribution, MenuContribution, MenuModelRegistry, nls
} from '@theia/core/lib/common';
import { Range, Position, SymbolInformation, WorkspaceSymbolParams } from '@theia/core/shared/vscode-languageserver-protocol';
import { MonacoLanguages, WorkspaceSymbolProvider } from './monaco-languages';
import URI from '@theia/core/lib/common/uri';
import { EditorMainMenu } from '@theia/editor/lib/browser';
@injectable()
export class WorkspaceSymbolCommand implements QuickAccessProvider, CommandContribution, KeybindingContribution, MenuContribution, CommandHandler, QuickAccessContribution {
public static readonly PREFIX = '#';
private command = Command.toDefaultLocalizedCommand({
id: 'languages.workspace.symbol',
label: 'Go to Symbol in Workspace...'
});
@inject(MonacoLanguages) protected readonly languages: MonacoLanguages;
@inject(OpenerService) protected readonly openerService: OpenerService;
@inject(QuickInputService) protected quickInputService: QuickInputService;
@inject(QuickAccessRegistry) protected quickAccessRegistry: QuickAccessRegistry;
@inject(SelectionService) protected selectionService: SelectionService;
@inject(LabelProvider) protected readonly labelProvider: LabelProvider;
isEnabled(): boolean {
return this.languages.workspaceSymbolProviders !== undefined;
}
execute(): void {
this.quickInputService.open(WorkspaceSymbolCommand.PREFIX);
}
registerCommands(commands: CommandRegistry): void {
commands.registerCommand(this.command, this);
}
registerMenus(menus: MenuModelRegistry): void {
menus.registerMenuAction(EditorMainMenu.WORKSPACE_GROUP, {
commandId: this.command.id,
order: '2'
});
}
private isElectron(): boolean {
return environment.electron.is();
}
registerKeybindings(keybindings: KeybindingRegistry): void {
keybindings.registerKeybinding({
command: this.command.id,
keybinding: this.isElectron() ? 'ctrlcmd+t' : 'ctrlcmd+o',
});
}
registerQuickAccessProvider(): void {
this.quickAccessRegistry.registerQuickAccessProvider({
getInstance: () => this,
prefix: WorkspaceSymbolCommand.PREFIX,
placeholder: '',
helpEntries: [{ description: nls.localizeByDefault('Go to Symbol in Workspace'), needsEditor: false }]
});
}
async getPicks(filter: string, token: CancellationToken): Promise<QuickPicks> {
const items: QuickPicks = [];
if (this.languages.workspaceSymbolProviders) {
const param: WorkspaceSymbolParams = {
query: filter
};
const workspaceProviderPromises = [];
for (const provider of this.languages.workspaceSymbolProviders) {
workspaceProviderPromises.push((async () => {
const symbols = await provider.provideWorkspaceSymbols(param, token);
if (symbols && !token.isCancellationRequested) {
for (const symbol of symbols) {
items.push(this.createItem(symbol, provider, filter, token));
}
}
return symbols;
})());
}
await Promise.all(workspaceProviderPromises.map(p => p.then(sym => sym, _ => undefined)))
.then(symbols => {
const filteredSymbols = symbols.filter(el => el && el.length !== 0);
if (filteredSymbols.length === 0) {
items.push({
label: filter.length === 0
? nls.localize('theia/monaco/typeToSearchForSymbols', 'Type to search for symbols')
: nls.localize('theia/monaco/noSymbolsMatching', 'No symbols matching'),
});
}
}).catch();
}
return items;
}
protected createItem(sym: SymbolInformation, provider: WorkspaceSymbolProvider, filter: string, token: CancellationToken): QuickPickItem {
const uri = new URI(sym.location.uri);
const iconClasses = this.toCssClassName(sym.kind);
let parent = sym.containerName;
if (parent) {
parent += ' - ';
}
const description = (parent || '') + this.labelProvider.getName(uri);
return ({
label: sym.name,
description,
ariaLabel: uri.toString(),
iconClasses,
highlights: {
label: findMatches(sym.name, filter),
description: findMatches(description, filter)
},
execute: () => {
if (provider.resolveWorkspaceSymbol) {
provider.resolveWorkspaceSymbol(sym, token).then(resolvedSymbol => {
if (resolvedSymbol) {
this.openURL(uri, resolvedSymbol.location.range.start, resolvedSymbol.location.range.end);
} else {
// the symbol didn't resolve -> use given symbol
this.openURL(uri, sym.location.range.start, sym.location.range.end);
}
});
} else {
// resolveWorkspaceSymbol wasn't specified
this.openURL(uri, sym.location.range.start, sym.location.range.end);
}
}
});
}
protected toCssClassName(symbolKind: SymbolKind, inline?: boolean): string[] | undefined {
const kind = SymbolKind[symbolKind];
if (!kind) {
return undefined;
}
return ['codicon', `${inline ? 'inline' : 'block'}`, `codicon-symbol-${kind.toLowerCase() || 'property'}`];
}
private openURL(uri: URI, start: Position, end: Position): void {
this.openerService.getOpener(uri).then(opener => opener.open(uri, {
selection: Range.create(start, end)
}));
}
}
enum SymbolKind {
File = 1,
Module = 2,
Namespace = 3,
Package = 4,
Class = 5,
Method = 6,
Property = 7,
Field = 8,
Constructor = 9,
Enum = 10,
Interface = 11,
Function = 12,
Variable = 13,
Constant = 14,
String = 15,
Number = 16,
Boolean = 17,
Array = 18,
Object = 19,
Key = 20,
Null = 21,
EnumMember = 22,
Struct = 23,
Event = 24,
Operator = 25,
TypeParameter = 26
}

View File

@@ -0,0 +1,28 @@
// *****************************************************************************
// Copyright (C) 2017 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
/* note: this bogus test file is required so that
we are able to run mocha unit tests on this
package, without having any actual unit tests in it.
This way a coverage report will be generated,
showing 0% coverage, instead of no report.
This file can be removed once we have real unit
tests in place. */
describe('monaco package', () => {
it('support code coverage statistics', () => true);
});

View File

@@ -0,0 +1,31 @@
{
"extends": "../../configs/base.tsconfig",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib"
},
"include": [
"src"
],
"references": [
{
"path": "../core"
},
{
"path": "../editor"
},
{
"path": "../filesystem"
},
{
"path": "../markers"
},
{
"path": "../outline-view"
},
{
"path": "../workspace"
}
]
}