From 3419c3fc059ec00b874e1cb13fc72ca2555fb853 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Tue, 10 Jul 2018 14:28:09 +0200 Subject: [PATCH] GH-1845: Extended the LSP with the semantic highlighting capabilities. Closes #1845 Signed-off-by: Akos Kitta --- packages/core/src/common/disposable.ts | 5 + .../src/browser/editor-frontend-module.ts | 3 + .../semantic-highlighting-service.ts | 39 ++++ .../nsfw-watcher/nsfw-filesystem-watcher.ts | 2 +- .../src/browser/java-client-contribution.ts | 33 +++- packages/java/src/browser/java-protocol.ts | 20 ++ .../java-textmate-contribution.ts | 51 +++-- packages/java/src/node/java-backend-module.ts | 6 +- .../java/src/node/java-cli-contribution.ts | 58 ++++++ packages/java/src/node/java-contribution.ts | 33 +++- .../src/browser/monaco-frontend-module.ts | 5 + .../monaco-semantic-highlighting-service.ts | 186 ++++++++++++++++++ 12 files changed, 408 insertions(+), 33 deletions(-) create mode 100644 packages/editor/src/browser/semantic-highlight/semantic-highlighting-service.ts create mode 100644 packages/java/src/node/java-cli-contribution.ts create mode 100644 packages/monaco/src/browser/monaco-semantic-highlighting-service.ts diff --git a/packages/core/src/common/disposable.ts b/packages/core/src/common/disposable.ts index 269807fe82990..a62e10e7a5af4 100644 --- a/packages/core/src/common/disposable.ts +++ b/packages/core/src/common/disposable.ts @@ -32,9 +32,14 @@ export namespace Disposable { } export class DisposableCollection implements Disposable { + protected readonly disposables: Disposable[] = []; protected readonly onDisposeEmitter = new Emitter(); + constructor(...toDispose: Disposable[]) { + toDispose.forEach(d => this.push(d)); + } + get onDispose(): Event { return this.onDisposeEmitter.event; } diff --git a/packages/editor/src/browser/editor-frontend-module.ts b/packages/editor/src/browser/editor-frontend-module.ts index 022b10149ca9a..067f190e1c07f 100644 --- a/packages/editor/src/browser/editor-frontend-module.ts +++ b/packages/editor/src/browser/editor-frontend-module.ts @@ -31,6 +31,7 @@ import { NavigationLocationUpdater } from './navigation/navigation-location-upda import { NavigationLocationService } from './navigation/navigation-location-service'; import { NavigationLocationSimilarity } from './navigation/navigation-location-similarity'; import { EditorVariableContribution } from './editor-variable-contribution'; +import { SemanticHighlightingService } from './semantic-highlight/semantic-highlighting-service'; export default new ContainerModule(bind => { bindEditorPreferences(bind); @@ -58,4 +59,6 @@ export default new ContainerModule(bind => { bind(NavigationLocationSimilarity).toSelf().inSingletonScope(); bind(VariableContribution).to(EditorVariableContribution).inSingletonScope(); + + bind(SemanticHighlightingService).toSelf().inSingletonScope(); }); diff --git a/packages/editor/src/browser/semantic-highlight/semantic-highlighting-service.ts b/packages/editor/src/browser/semantic-highlight/semantic-highlighting-service.ts new file mode 100644 index 0000000000000..d2eb3e22a3504 --- /dev/null +++ b/packages/editor/src/browser/semantic-highlight/semantic-highlighting-service.ts @@ -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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable } from 'inversify'; +import { Position, Range } from 'vscode-languageserver-types'; +import URI from '@theia/core/lib/common/uri'; +import { Disposable } from '@theia/core/lib/common/disposable'; + +@injectable() +export class SemanticHighlightingService implements Disposable { + + async decorate(uri: URI, ranges: SemanticHighlightingRange[]): Promise { + // NOOP + } + + dispose(): void { + // NOOP + } + +} + +export interface SemanticHighlightingRange extends Range { + readonly scopes: string[]; +} + +export { Position, Range }; diff --git a/packages/filesystem/src/node/nsfw-watcher/nsfw-filesystem-watcher.ts b/packages/filesystem/src/node/nsfw-watcher/nsfw-filesystem-watcher.ts index 45181e113a4f0..cc8a1973f00e2 100644 --- a/packages/filesystem/src/node/nsfw-watcher/nsfw-filesystem-watcher.ts +++ b/packages/filesystem/src/node/nsfw-watcher/nsfw-filesystem-watcher.ts @@ -130,7 +130,7 @@ export class NsfwFileSystemWatcherServer implements FileSystemWatcherServer { this.watchers.delete(watcherId); this.debug('Stopping watching:', basePath); watcher.stop(); - this.options.info('Stopped watching.'); + this.options.info('Stopped watching:', basePath); }); this.watcherOptions.set(watcherId, { ignored: options.ignored.map(pattern => new Minimatch(pattern)) diff --git a/packages/java/src/browser/java-client-contribution.ts b/packages/java/src/browser/java-client-contribution.ts index 67274d5eec5b1..b7480f885a2ac 100644 --- a/packages/java/src/browser/java-client-contribution.ts +++ b/packages/java/src/browser/java-client-contribution.ts @@ -19,9 +19,19 @@ import { CommandService } from "@theia/core/lib/common"; import { Window, ILanguageClient, BaseLanguageClientContribution, Workspace, Languages, LanguageClientFactory, LanguageClientOptions } from '@theia/languages/lib/browser'; +import URI from '@theia/core/lib/common/uri'; +import { SemanticHighlightingService, SemanticHighlightingRange, Position } from "@theia/editor/lib/browser/semantic-highlight/semantic-highlighting-service"; import { JAVA_LANGUAGE_ID, JAVA_LANGUAGE_NAME } from '../common'; -import { ActionableNotification, ActionableMessage, StatusReport, StatusNotification } from "./java-protocol"; import { StatusBar, StatusBarEntry, StatusBarAlignment } from "@theia/core/lib/browser"; +import { + ActionableNotification, + ActionableMessage, + StatusReport, + StatusNotification, + SemanticHighlight, + SemanticHighlightingParams, + SemanticHighlightingToken +} from "./java-protocol"; @injectable() export class JavaClientContribution extends BaseLanguageClientContribution { @@ -37,7 +47,8 @@ export class JavaClientContribution extends BaseLanguageClientContribution { @inject(LanguageClientFactory) protected readonly languageClientFactory: LanguageClientFactory, @inject(Window) protected readonly window: Window, @inject(CommandService) protected readonly commandService: CommandService, - @inject(StatusBar) protected readonly statusBar: StatusBar + @inject(StatusBar) protected readonly statusBar: StatusBar, + @inject(SemanticHighlightingService) protected readonly semanticHighlightingService: SemanticHighlightingService ) { super(workspace, languages, languageClientFactory); } @@ -53,6 +64,7 @@ export class JavaClientContribution extends BaseLanguageClientContribution { protected onReady(languageClient: ILanguageClient): void { languageClient.onNotification(ActionableNotification.type, this.showActionableMessage.bind(this)); languageClient.onNotification(StatusNotification.type, this.showStatusMessage.bind(this)); + languageClient.onNotification(SemanticHighlight.type, this.applySemanticHighlighting.bind(this)); super.onReady(languageClient); } @@ -73,6 +85,20 @@ export class JavaClientContribution extends BaseLanguageClientContribution { }, 5000); } + protected applySemanticHighlighting(params: SemanticHighlightingParams): void { + const toRanges: (tuple: [number, SemanticHighlightingToken[]]) => SemanticHighlightingRange[] = tuple => { + const [line, tokens] = tuple; + return tokens.map(token => ({ + start: Position.create(line, token.character), + end: Position.create(line, token.character + token.length), + scopes: token.scopes + })); + }; + const ranges = params.lines.map(line => [line.line, line.tokens]).map(toRanges).reduce((acc, current) => acc.concat(current), []); + const uri = new URI(params.uri); + this.semanticHighlightingService.decorate(uri, ranges); + } + protected showActionableMessage(message: ActionableMessage): void { const items = message.commands || []; this.window.showMessage(message.severity, message.message, ...items).then(command => { @@ -87,7 +113,8 @@ export class JavaClientContribution extends BaseLanguageClientContribution { const options = super.createOptions(); options.initializationOptions = { extendedClientCapabilities: { - classFileContentsSupport: true + classFileContentsSupport: true, + semanticHighlighting: true } }; return options; diff --git a/packages/java/src/browser/java-protocol.ts b/packages/java/src/browser/java-protocol.ts index 79be5c42e4545..0561466a3241b 100644 --- a/packages/java/src/browser/java-protocol.ts +++ b/packages/java/src/browser/java-protocol.ts @@ -33,6 +33,22 @@ export interface ActionableMessage { commands?: Command[]; } +export interface SemanticHighlightingParams { + readonly uri: string; + readonly lines: SemanticHighlightingInformation[]; +} + +export interface SemanticHighlightingInformation { + readonly line: number; + readonly tokens: SemanticHighlightingToken[]; +} + +export interface SemanticHighlightingToken { + readonly character: number; + readonly length: number; + readonly scopes: string[]; +} + export namespace ClassFileContentsRequest { export const type = new RequestType('java/classFileContents'); } @@ -40,3 +56,7 @@ export namespace ClassFileContentsRequest { export namespace ActionableNotification { export const type = new NotificationType('language/actionableNotification'); } + +export namespace SemanticHighlight { + export const type = new NotificationType('textDocument/semanticHighlighting'); +} diff --git a/packages/java/src/browser/monaco-contribution/java-textmate-contribution.ts b/packages/java/src/browser/monaco-contribution/java-textmate-contribution.ts index fc38938760f90..65c0f306c7f58 100644 --- a/packages/java/src/browser/monaco-contribution/java-textmate-contribution.ts +++ b/packages/java/src/browser/monaco-contribution/java-textmate-contribution.ts @@ -15,33 +15,42 @@ ********************************************************************************/ import { injectable } from 'inversify'; -import { JAVA_LANGUAGE_ID } from '../../common'; +// import { JAVA_LANGUAGE_ID } from '../../common'; import { LanguageGrammarDefinitionContribution, TextmateRegistry } from '@theia/monaco/lib/browser/textmate'; @injectable() export class JavaTextmateContribution implements LanguageGrammarDefinitionContribution { + // TODO: restore this! registerTextmateLanguage(registry: TextmateRegistry) { - const javaDocGrammar = require('../../../data/javadoc.tmlanguage.json'); - registry.registerTextMateGrammarScope('text.html.javadoc', { - async getGrammarDefinition() { - return { - format: 'json', - content: javaDocGrammar - }; - } - }); - const scope = 'source.java'; - const javaGrammar = require('../../../data/java.tmlanguage.json'); - registry.registerTextMateGrammarScope(scope, { - async getGrammarDefinition() { - return { - format: 'json', - content: javaGrammar - }; - } - }); + // registry.registerTextMateGrammarScope('text.html.basic', { + // async getGrammarDefinition() { + // return { + // format: 'json', + // content: '{}' + // }; + // } + // }); + // const javaDocGrammar = require('../../../data/javadoc.tmlanguage.json'); + // registry.registerTextMateGrammarScope('text.html.javadoc', { + // async getGrammarDefinition() { + // return { + // format: 'json', + // content: javaDocGrammar + // }; + // } + // }); + // const scope = 'source.java'; + // const javaGrammar = require('../../../data/java.tmlanguage.json'); + // registry.registerTextMateGrammarScope(scope, { + // async getGrammarDefinition() { + // return { + // format: 'json', + // content: javaGrammar + // }; + // } + // }); - registry.mapLanguageIdToTextmateGrammar(JAVA_LANGUAGE_ID, scope); + // registry.mapLanguageIdToTextmateGrammar(JAVA_LANGUAGE_ID, scope); } } diff --git a/packages/java/src/node/java-backend-module.ts b/packages/java/src/node/java-backend-module.ts index 35c37e50072b6..1424d0184b963 100644 --- a/packages/java/src/node/java-backend-module.ts +++ b/packages/java/src/node/java-backend-module.ts @@ -15,9 +15,13 @@ ********************************************************************************/ import { ContainerModule } from "inversify"; -import { LanguageServerContribution } from "@theia/languages/lib/node"; +import { CliContribution } from '@theia/core/lib/node/cli'; +import { LanguageServerContribution } from '@theia/languages/lib/node'; import { JavaContribution } from './java-contribution'; +import { JavaCliContribution } from './java-cli-contribution'; export default new ContainerModule(bind => { bind(LanguageServerContribution).to(JavaContribution).inSingletonScope(); + bind(JavaCliContribution).toSelf().inSingletonScope(); + bind(CliContribution).toService(JavaCliContribution); }); diff --git a/packages/java/src/node/java-cli-contribution.ts b/packages/java/src/node/java-cli-contribution.ts new file mode 100644 index 0000000000000..fa4a4d7c9a6d3 --- /dev/null +++ b/packages/java/src/node/java-cli-contribution.ts @@ -0,0 +1,58 @@ +/******************************************************************************** + * 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable } from 'inversify'; +import { Argv, Arguments } from 'yargs'; +import { CliContribution } from '@theia/core/src/node/cli'; + +@injectable() +export class JavaCliContribution implements CliContribution { + + private static LS_PORT = 'java-ls'; + + protected _lsPort: number | undefined; + + configure(conf: Argv): void { + conf.option(JavaCliContribution.LS_PORT, { + description: 'Can be specified if the backend should not start the Java LS process but create a socket server and wait until the Java LS connects.', + type: 'number', + nargs: 1 + }); + } + + setArguments(args: Arguments): void { + this.setLsPort(args[JavaCliContribution.LS_PORT]); + } + + lsPort(): number | undefined { + return this._lsPort; + } + + // tslint:disable-next-line:no-any + protected setLsPort(port: any): void { + if (port !== undefined) { + const error = new Error(`The port for the Java LS must be an integer between 1 and 65535. It was: ${port}.`); + if (!Number.isInteger(port)) { + throw error; + } + if (port < 1 || port > 65535) { + throw error; + } + this._lsPort = port; + } + } + +} diff --git a/packages/java/src/node/java-contribution.ts b/packages/java/src/node/java-contribution.ts index 75f4e20a08ef2..06c18d1069d6d 100644 --- a/packages/java/src/node/java-contribution.ts +++ b/packages/java/src/node/java-contribution.ts @@ -17,10 +17,13 @@ import * as os from 'os'; import * as path from 'path'; import * as glob from 'glob'; -import { injectable } from "inversify"; +import { Socket } from 'net'; +import { injectable, inject } from "inversify"; +import { createSocketConnection } from 'vscode-ws-jsonrpc/lib/server'; import { DEBUG_MODE } from '@theia/core/lib/node'; import { IConnection, BaseLanguageServerContribution } from "@theia/languages/lib/node"; import { JAVA_LANGUAGE_ID, JAVA_LANGUAGE_NAME } from '../common'; +import { JavaCliContribution } from './java-cli-contribution'; export type ConfigurationType = 'config_win' | 'config_mac' | 'config_linux'; export const configurations = new Map(); @@ -31,10 +34,23 @@ configurations.set('linux', 'config_linux'); @injectable() export class JavaContribution extends BaseLanguageServerContribution { + @inject(JavaCliContribution) + protected readonly cli: JavaCliContribution; + readonly id = JAVA_LANGUAGE_ID; readonly name = JAVA_LANGUAGE_NAME; start(clientConnection: IConnection): void { + + const socketPort = this.cli.lsPort(); + if (socketPort) { + const socket = new Socket(); + const serverConnection = createSocketConnection(socket, socket, () => socket.destroy()); + this.forward(clientConnection, serverConnection); + socket.connect(socketPort); + return; + } + const serverPath = path.resolve(__dirname, '..', '..', 'server'); const jarPaths = glob.sync('**/plugins/org.eclipse.equinox.launcher_*.jar', { cwd: serverPath }); if (jarPaths.length === 0) { @@ -49,17 +65,20 @@ export class JavaContribution extends BaseLanguageServerContribution { } const configurationPath = path.resolve(serverPath, configuration); const command = 'java'; - const args = [ + const args: string[] = []; + + if (DEBUG_MODE) { + args.push('-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=1044'); + } + + args.push(...[ '-Declipse.application=org.eclipse.jdt.ls.core.id1', '-Dosgi.bundles.defaultStartLevel=4', '-Declipse.product=org.eclipse.jdt.ls.core.product' - ]; + ]); if (DEBUG_MODE) { - args.push( - '-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=1044', - '-Dlog.level=ALL' - ); + args.push('-Dlog.level=ALL'); } args.push( diff --git a/packages/monaco/src/browser/monaco-frontend-module.ts b/packages/monaco/src/browser/monaco-frontend-module.ts index 20fc06ab9b1e2..35487baf2d027 100644 --- a/packages/monaco/src/browser/monaco-frontend-module.ts +++ b/packages/monaco/src/browser/monaco-frontend-module.ts @@ -40,6 +40,8 @@ import { MonacoStrictEditorTextFocusContext } from './monaco-keybinding-contexts import { MonacoFrontendApplicationContribution } from './monaco-frontend-application-contribution'; import MonacoTextmateModuleBinder from './textmate/monaco-textmate-frontend-bindings'; import { QuickInputService } from './monaco-quick-input-service'; +import { MonacoSemanticHighlightingService } from './monaco-semantic-highlighting-service'; +import { SemanticHighlightingService } from '@theia/editor/lib/browser/semantic-highlight/semantic-highlighting-service'; decorate(injectable(), MonacoToProtocolConverter); decorate(injectable(), ProtocolToMonacoConverter); @@ -94,4 +96,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { MonacoTextmateModuleBinder(bind, unbind, isBound, rebind); bind(QuickInputService).toSelf().inSingletonScope(); + + bind(MonacoSemanticHighlightingService).toSelf().inSingletonScope(); + rebind(SemanticHighlightingService).to(MonacoSemanticHighlightingService).inSingletonScope(); }); diff --git a/packages/monaco/src/browser/monaco-semantic-highlighting-service.ts b/packages/monaco/src/browser/monaco-semantic-highlighting-service.ts new file mode 100644 index 0000000000000..373f3586f1f34 --- /dev/null +++ b/packages/monaco/src/browser/monaco-semantic-highlighting-service.ts @@ -0,0 +1,186 @@ +/******************************************************************************** + * 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { inject, injectable } from 'inversify'; +import URI from '@theia/core/lib/common/uri'; +import { ILogger } from '@theia/core/lib/common/logger'; +import { EditorManager } from '@theia/editor/lib/browser/editor-manager'; +import { TextDocumentChangeEvent } from '@theia/editor/lib/browser/editor'; +import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; +import { EditorDecoration, EditorDecorationOptions } from '@theia/editor/lib/browser/decorations'; +import { SemanticHighlightingService, SemanticHighlightingRange, Range } from '@theia/editor/lib/browser/semantic-highlight/semantic-highlighting-service'; +import { MonacoEditor } from './monaco-editor'; +import ITextModel = monaco.editor.ITextModel; +import TokenMetadata = monaco.modes.TokenMetadata; +import StaticServices = monaco.services.StaticServices; + +@injectable() +export class MonacoSemanticHighlightingService extends SemanticHighlightingService { + + @inject(ILogger) + protected readonly logger: ILogger; + + @inject(EditorManager) + protected readonly editorManager: EditorManager; + + protected readonly decorations = new Map(); + protected readonly toDisposeOnEditorClose = new Map(); + + async decorate(uri: URI, ranges: SemanticHighlightingRange[]): Promise { + const editor = await this.editor(uri); + if (!editor) { + return; + } + + const key = uri.toString(); + if (!this.toDisposeOnEditorClose.has(key)) { + this.toDisposeOnEditorClose.set(key, new DisposableCollection( + editor.onDispose(() => this.deleteDecorations(key, editor)), + editor.onDocumentContentChanged(this.documentContentChanged.bind(this)) + )); + } + // XXX: Why cannot TS infer this type? Perhaps `EditorDecorationOptions` has only optional properties. Just guessing though. + const newDecorations: EditorDecoration[] = ranges.map(this.toDecoration.bind(this)); + const newState = editor.deltaDecorations({ + newDecorations, + oldDecorations: [] + }); + + // Cache the new state. + this.decorations.set(key, { + ranges, + decorations: newState + }); + } + + dispose(): void { + Array.from(this.toDisposeOnEditorClose.values()).forEach(disposable => disposable.dispose()); + } + + protected async editor(uri: string | URI): Promise { + const editorWidget = await this.editorManager.getByUri(typeof uri === 'string' ? new URI(uri) : uri); + if (!!editorWidget && editorWidget.editor instanceof MonacoEditor) { + return editorWidget.editor; + } + return undefined; + } + + protected async model(uri: string | URI): Promise { + const editor = await this.editor(uri); + if (editor) { + return editor.getControl().getModel(); + } + return undefined; + } + + /** + * Returns with a range between `from` and `to`: `[from, from + 1, ..., to - 1, to]`. + * - `from` can be greater than `to`. If so, the arguments will be swapped internally and you still get back `[to, to + 1, ..., from -1, from]` + * - If `from` and `to` are the same, returns with a single element range: `[from]`. + */ + protected rangeOf(from: number, to: number): number[] { + const max = Math.max(from, to); + const min = Math.min(from, to); + const range: number[] = []; + for (let i = min; i <= max; i++) { + range.push(i); + } + return range; + } + + /** + * We do not get delta notification from the LS if lines were deleted and new semantic highlighting positions were introduced. + * We need to track deletion here and get rid of the affected markers manually. Note, deletion can be an insert edit where the + * replaced content is "less" than the original content. + */ + protected async documentContentChanged(event: TextDocumentChangeEvent): Promise { + const editor = await this.editor(event.document.uri); + if (editor) { + const model = editor.getControl().getModel(); + const affectedLines = Array.from(new Set(event.contentChanges + .filter(change => change.text.length === 0) + .map(change => [change.range.start.line, change.range.end.line]) + .map(([from, to]) => this.rangeOf(from, to)) + .reduce((prev, curr) => prev.concat(curr), []))); + const oldDecorations = Array.from(affectedLines) + .map(line => model.getLineDecorations(line + 1, undefined, true)) // p2m + .reduce((prev, curr) => prev.concat(curr), []) + .map(decoration => decoration.id); + editor.deltaDecorations({ + newDecorations: [], + oldDecorations + }); + } + } + + protected deleteDecorations(uri: string, editor: MonacoEditor): void { + const decorationWithRanges = this.decorations.get(uri); + if (decorationWithRanges) { + const oldDecorations = decorationWithRanges.decorations; + editor.deltaDecorations({ + newDecorations: [], + oldDecorations + }); + this.decorations.delete(uri); + } + const disposable = this.toDisposeOnEditorClose.get(uri); + if (disposable) { + disposable.dispose(); + } + this.toDisposeOnEditorClose.delete(uri); + } + + protected toDecoration(range: SemanticHighlightingRange): EditorDecoration { + const { start, end } = range; + const options = this.toOptions(range.scopes); + return { + range: Range.create(start, end), + options + }; + } + + protected toOptions(scopes: string[]): EditorDecorationOptions { + // TODO: why for-of? How to pick the right scope? Is it fine to get the first element (with the narrowest scope)? + for (const scope of scopes) { + const metadata = this.tokenTheme().match(undefined, scope); + const inlineClassName = TokenMetadata.getClassNameFromMetadata(metadata); + return { + inlineClassName + }; + } + return {}; + } + + protected tokenTheme(): monaco.services.TokenTheme { + return StaticServices.standaloneThemeService.get().getTheme().tokenTheme; + } + +} + +/** + * Helper tuple type with text editor decoration IDs and the raw highlighting ranges. + */ +export interface DecorationWithRanges { + readonly decorations: string[]; + readonly ranges: SemanticHighlightingRange[]; +} + +export namespace DecorationWithRanges { + export const EMPTY: DecorationWithRanges = { + decorations: [], + ranges: [] + }; +}