Skip to content

Commit

Permalink
GH-1845: Extended the LSP with the semantic highlighting capabilities.
Browse files Browse the repository at this point in the history
Closes #1845

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
  • Loading branch information
Akos Kitta committed Jul 10, 2018
1 parent 1fc36d0 commit 2eeaabd
Show file tree
Hide file tree
Showing 9 changed files with 323 additions and 10 deletions.
7 changes: 7 additions & 0 deletions packages/core/src/common/disposable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,16 @@ export namespace Disposable {
}

export class DisposableCollection implements Disposable {

protected readonly disposables: Disposable[] = [];
protected readonly onDisposeEmitter = new Emitter<void>();

constructor(toDispose?: Disposable | Disposable[]) {
if (!!toDispose) {
(Array.isArray(toDispose) ? toDispose : [toDispose]).forEach(d => this.push(d));
}
}

get onDispose(): Event<void> {
return this.onDisposeEmitter.event;
}
Expand Down
4 changes: 4 additions & 0 deletions packages/editor/src/browser/editor-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, NoopSemanticHighlightingService } from './semantic-highlight/semantic-highlighting-service';

export default new ContainerModule(bind => {
bindEditorPreferences(bind);
Expand Down Expand Up @@ -58,4 +59,7 @@ export default new ContainerModule(bind => {
bind(NavigationLocationSimilarity).toSelf().inSingletonScope();

bind(VariableContribution).to(EditorVariableContribution).inSingletonScope();

bind(NoopSemanticHighlightingService).toSelf().inSingletonScope();
bind(SemanticHighlightingService).to(NoopSemanticHighlightingService).inSingletonScope();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/********************************************************************************
* 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';

export const SemanticHighlightingService = Symbol('SemanticHighlightingService');
export interface SemanticHighlightingService extends Disposable {

decorate(uri: URI, ranges: SemanticHighlightingRange[]): Promise<void>;

}

@injectable()
export class NoopSemanticHighlightingService implements SemanticHighlightingService {

async decorate(): Promise<void> {
// NOOP
}

dispose(): void {
// NOOP
}

}

export interface SemanticHighlightingRange extends Range {
readonly scopes: string[];
}

export { Position, Range };
33 changes: 30 additions & 3 deletions packages/java/src/browser/java-client-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);
}
Expand All @@ -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);
}

Expand All @@ -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 => {
Expand All @@ -87,7 +113,8 @@ export class JavaClientContribution extends BaseLanguageClientContribution {
const options = super.createOptions();
options.initializationOptions = {
extendedClientCapabilities: {
classFileContentsSupport: true
classFileContentsSupport: true,
semanticHighlighting: true
}
};
return options;
Expand Down
20 changes: 20 additions & 0 deletions packages/java/src/browser/java-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,30 @@ 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<TextDocumentIdentifier, string | undefined, void, void>('java/classFileContents');
}

export namespace ActionableNotification {
export const type = new NotificationType<ActionableMessage, void>('language/actionableNotification');
}

export namespace SemanticHighlight {
export const type = new NotificationType<SemanticHighlightingParams, void>('textDocument/semanticHighlighting');
}
19 changes: 12 additions & 7 deletions packages/java/src/node/java-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,22 @@ export class JavaContribution extends BaseLanguageServerContribution {
}
const configurationPath = path.resolve(serverPath, configuration);
const command = 'java';
const args = [
const args: string[] = [];
let debug = DEBUG_MODE;
// TODO! must not be hard-coded.
debug = true;
if (debug) {
args.push('-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,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'
);
if (debug) {
args.push('-Dlog.level=ALL');
}

args.push(
Expand Down
5 changes: 5 additions & 0 deletions packages/monaco/src/browser/monaco-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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();
});
169 changes: 169 additions & 0 deletions packages/monaco/src/browser/monaco-semantic-highlighting-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/********************************************************************************
* 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 { TextEditor } from '@theia/editor/lib/browser/editor';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { EditorDecoration, EditorDecorationOptions } from '@theia/editor/lib/browser/decorations';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { SemanticHighlightingService, SemanticHighlightingRange, Range } from '@theia/editor/lib/browser/semantic-highlight/semantic-highlighting-service';
import { MonacoEditor } from './monaco-editor';
import StaticServices = monaco.services.StaticServices;
import Color = monaco.services.Color;
import TokenMetadata = monaco.modes.TokenMetadata;

@injectable()
export class MonacoSemanticHighlightingService implements SemanticHighlightingService {

@inject(ILogger)
protected readonly logger: ILogger;

@inject(EditorManager)
protected readonly editorManager: EditorManager;

protected readonly toDisposeOnEditorClose = new Map<string, Disposable>();
protected readonly decorations = new Map<string, DecorationWithRanges>();
protected readonly colorMapCache = new Map<string, string>();

async decorate(uri: URI, ranges: SemanticHighlightingRange[]): Promise<void> {
const editorWidget = await this.editorManager.getByUri(uri);
if (!editorWidget) {
return;
}
if (!(editorWidget.editor instanceof MonacoEditor)) {
return;
}

const editor = editorWidget.editor;
const key = uri.toString();
if (!this.toDisposeOnEditorClose.has(key)) {
this.toDisposeOnEditorClose.set(key, new DisposableCollection([
editor.onDispose((() => this.deleteDecorations(key, editor))),
Disposable.create(() => this.toDisposeOnEditorClose.delete(key))
]));
}

// Merge the previous state with the current one. Collect all affected lines based on the new state.
const affectedLines = new Set(ranges.map(r => r.start.line));
const oldState = this.decorations.get(key) || DecorationWithRanges.EMPTY;
// Discard all ranges from the previous state that are from an affected line from the new state.
// And merge them with the ranges from the new state. We cache them together.
const rangesToCache = oldState.ranges.filter(r => !affectedLines.has(r.start.line)).concat(ranges);
// XXX: Why cannot TS infer this type? Perhaps `EditorDecorationOptions` has only optional properties. Just guessing though.
const newDecorations: EditorDecoration[] = rangesToCache.map(this.toDecoration.bind(this));
const oldDecorations = oldState.decorations;
// Do the decorations.
const newState = editor.deltaDecorations({
newDecorations,
oldDecorations
});

// Cache the new state.
this.decorations.set(key, {
ranges: rangesToCache,
decorations: newState
});
}

dispose(): void {
Array.from(this.toDisposeOnEditorClose.values()).forEach(disposable => disposable.dispose());
}

protected async deleteDecorations(uri: string, editor: TextEditor): Promise<void> {
const decorationWithRanges = this.decorations.get(uri);
if (decorationWithRanges) {
const oldDecorations = decorationWithRanges.decorations;
editor.deltaDecorations({
newDecorations: [],
oldDecorations
});
this.decorations.delete(uri);
}
}

protected toDecoration(range: SemanticHighlightingRange): EditorDecoration {
// const { start, end } = range;
// XXX: This is a hack to be able to see the decorations.
const start = {
line: range.start.line,
character: range.start.character + 2
};
const end = {
line: range.end.line,
character: range.end.character + 2
};
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(scope);
// TODO: enable some sort of caching.
// const colorMap: string[] = this.colorMap().map(this.toColorString.bind(this));
const inlineClassName = TokenMetadata.getClassNameFromMetadata(metadata);
return {
inlineClassName
};
}
return {};
}

protected toColorString(color: Color): string {
// TODO: get rid of this and the color cache map and other obsolete code (including the index.d.ts) if not needed.
const { r, g, b } = color.rgba;
return `#${[r, g, b].map(c => c.toString(16)).map(c => c.length < 2 ? `0${c}` : c).join('')}`;
}

protected colorMapIndex(...scopes: string[]) {
scopes.map(scope => {
const themeTrie = this.tokenTheme()._match(scope);
const foreground = TokenMetadata.getForeground(themeTrie.metadata);
const background = TokenMetadata.getBackground(themeTrie.metadata);
console.log(`scope: ${scope} => ${themeTrie} => ${foreground}:${background}`);
});
}

protected colorMap(): monaco.services.Color[] {
return this.tokenTheme().getColorMap();
}

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: []
};
}
Loading

0 comments on commit 2eeaabd

Please sign in to comment.