Skip to content

Commit 13d272d

Browse files
authored
Merge pull request #49 from CodinGame/refactor-client-extensions
Refactor client extensions, add java inlay hints
2 parents dd52726 + 86d9b0b commit 13d272d

12 files changed

+198
-84
lines changed

package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"@codingame/monaco-editor-wrapper": "^1.14.5",
2727
"@codingame/monaco-jsonrpc": "^0.4.0",
2828
"sweetalert": "^2.1.2",
29-
"vscode": "npm:@codingame/monaco-vscode-api@^1.68.3",
29+
"vscode": "npm:@codingame/monaco-vscode-api@^1.68.4",
3030
"vscode-languageserver-protocol": "^3.17.1"
3131
},
3232
"devDependencies": {

src/createLanguageClient.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import { MessageReader, MessageWriter, Message, Event, DataCallback, Disposable, PartialMessageInfo } from 'vscode-jsonrpc'
22
import { Uri } from 'monaco-editor'
33
import {
4-
MonacoLanguageClient, Middleware, ErrorHandler, IConnectionProvider, LanguageClientOptions, MessageTransports
4+
MonacoLanguageClient, Middleware, ErrorHandler, IConnectionProvider, MessageTransports
55
} from 'monaco-languageclient'
66
import { InitializeParams, InitializeRequest, RegistrationParams, RegistrationRequest, UnregistrationParams, UnregistrationRequest } from 'vscode-languageserver-protocol'
7-
import { registerExtensionFeatures } from './extensions'
8-
import { LanguageClientId } from './languageClientOptions'
7+
import { LanguageClientId, LanguageClientOptions } from './languageClientOptions'
98
import { Infrastructure } from './infrastructure'
109

1110
interface MessageMiddleware {
@@ -141,7 +140,8 @@ function createLanguageClient (
141140
{
142141
documentSelector,
143142
synchronize,
144-
initializationOptions
143+
initializationOptions,
144+
createAdditionalFeatures
145145
}: LanguageClientOptions,
146146
errorHandler: ErrorHandler,
147147
middleware?: Middleware
@@ -164,7 +164,9 @@ function createLanguageClient (
164164
client.registerProgressFeatures()
165165
client.registerTextDocumentSaveFeatures()
166166

167-
registerExtensionFeatures(client, id)
167+
if (createAdditionalFeatures != null) {
168+
client.registerFeatures(createAdditionalFeatures(client))
169+
}
168170

169171
return client
170172
}

src/extensions.ts

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { monaco, registerTextModelContentProvider } from '@codingame/monaco-edit
22
import {
33
Disposable, MonacoLanguageClient, DisposableCollection
44
} from 'monaco-languageclient'
5-
import { StaticFeature, FeatureState, ProtocolRequestType } from 'vscode-languageclient/lib/common/api'
5+
import { StaticFeature, FeatureState } from 'vscode-languageclient/lib/common/api'
66
import { DidSaveTextDocumentNotification, DocumentSelector, Emitter, ServerCapabilities, TextDocumentSyncOptions } from 'vscode-languageserver-protocol'
77
import * as vscode from 'vscode'
88
import { updateFile, willShutdownNotificationType, WillShutdownParams } from './customRequests'
@@ -57,47 +57,6 @@ export class InitializeTextDocumentFeature implements StaticFeature {
5757
}
5858
}
5959

60-
export const ResolveCobolSubroutineRequestType = new ProtocolRequestType<string, string, never, void, void>('cobol/resolveSubroutine')
61-
class CobolResolveSubroutineFeature implements StaticFeature {
62-
private onRequestDisposable: Disposable | undefined
63-
constructor (private languageClient: MonacoLanguageClient) {
64-
}
65-
66-
fillClientCapabilities (): void {}
67-
68-
initialize (capabilities: ServerCapabilities, documentSelector: DocumentSelector): void {
69-
this.onRequestDisposable = this.languageClient.onRequest(ResolveCobolSubroutineRequestType, (routineName: string): string => {
70-
const constantRoutinePaths: Partial<Record<string, string>> = {
71-
'assert-equals': `file:${vscode.workspace.rootPath ?? '/tmp/project'}/deps/assert-equals.cbl`
72-
}
73-
const contantRoutinePath = constantRoutinePaths[routineName.toLowerCase()]
74-
if (contantRoutinePath != null) {
75-
return contantRoutinePath
76-
}
77-
return vscode.workspace.textDocuments
78-
.filter(textDocument => vscode.languages.match(documentSelector, textDocument))
79-
.filter(document => document.getText().match(new RegExp(`PROGRAM-ID\\.\\W+${routineName}\\.`, 'gi')))
80-
.sort((a, b) => a.uri.toString().localeCompare(b.uri.toString()))[0]?.uri.toString()
81-
})
82-
}
83-
84-
getState (): FeatureState {
85-
return {
86-
kind: 'static'
87-
}
88-
}
89-
90-
dispose (): void {
91-
this.onRequestDisposable?.dispose()
92-
}
93-
}
94-
95-
export function registerExtensionFeatures (client: MonacoLanguageClient, language: string): void {
96-
if (language === 'cobol') {
97-
client.registerFeature(new CobolResolveSubroutineFeature(client))
98-
}
99-
}
100-
10160
export class WillDisposeFeature implements StaticFeature {
10261
constructor (private languageClient: MonacoLanguageClient, private onWillShutdownEmitter: Emitter<WillShutdownParams>) {}
10362
fillClientCapabilities (): void {}

src/extensions/cobol.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import {
2+
Disposable, MonacoLanguageClient
3+
} from 'monaco-languageclient'
4+
import { StaticFeature, FeatureState, ProtocolRequestType } from 'vscode-languageclient/lib/common/api'
5+
import { DocumentSelector, ServerCapabilities } from 'vscode-languageserver-protocol'
6+
import * as vscode from 'vscode'
7+
8+
export const ResolveCobolSubroutineRequestType = new ProtocolRequestType<string, string, never, void, void>('cobol/resolveSubroutine')
9+
export class CobolResolveSubroutineFeature implements StaticFeature {
10+
private onRequestDisposable: Disposable | undefined
11+
constructor (private languageClient: MonacoLanguageClient) {
12+
}
13+
14+
fillClientCapabilities (): void {}
15+
16+
initialize (capabilities: ServerCapabilities, documentSelector: DocumentSelector): void {
17+
this.onRequestDisposable = this.languageClient.onRequest(ResolveCobolSubroutineRequestType, (routineName: string): string => {
18+
const constantRoutinePaths: Partial<Record<string, string>> = {
19+
'assert-equals': `file:${vscode.workspace.rootPath ?? '/tmp/project'}/deps/assert-equals.cbl`
20+
}
21+
const contantRoutinePath = constantRoutinePaths[routineName.toLowerCase()]
22+
if (contantRoutinePath != null) {
23+
return contantRoutinePath
24+
}
25+
return vscode.workspace.textDocuments
26+
.filter(textDocument => vscode.languages.match(documentSelector, textDocument))
27+
.filter(document => document.getText().match(new RegExp(`PROGRAM-ID\\.\\W+${routineName}\\.`, 'gi')))
28+
.sort((a, b) => a.uri.toString().localeCompare(b.uri.toString()))[0]?.uri.toString()
29+
})
30+
}
31+
32+
getState (): FeatureState {
33+
return {
34+
kind: 'static'
35+
}
36+
}
37+
38+
dispose (): void {
39+
this.onRequestDisposable?.dispose()
40+
}
41+
}

src/extensions/java.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import {
2+
DisposableCollection, MonacoLanguageClient
3+
} from 'monaco-languageclient'
4+
import { StaticFeature, FeatureState } from 'vscode-languageclient/lib/common/api'
5+
import { InlayHint, InlayHintParams, InlayHintRefreshRequest, InlayHintRequest, WorkspaceEdit } from 'vscode-languageserver-protocol'
6+
import * as vscode from 'vscode'
7+
8+
async function asInlayHints (values: InlayHint[] | undefined | null, client: MonacoLanguageClient): Promise<vscode.InlayHint[] | undefined> {
9+
if (!Array.isArray(values)) {
10+
return undefined
11+
}
12+
return values.map(lsHint => asInlayHint(lsHint, client))
13+
}
14+
15+
function asInlayHint (value: InlayHint, client: MonacoLanguageClient): vscode.InlayHint {
16+
const label = value.label as string
17+
const result = new vscode.InlayHint(client.protocol2CodeConverter.asPosition(value.position), label)
18+
result.paddingRight = true
19+
result.kind = vscode.InlayHintKind.Parameter
20+
return result
21+
}
22+
23+
/**
24+
* Comes from https://github.com/redhat-developer/vscode-java/blob/9b6046eecc65fd47507f309a3ccc9add45c6d3be/src/inlayHintsProvider.ts#L5
25+
*/
26+
class JavaInlayHintsProvider implements vscode.InlayHintsProvider {
27+
private onDidChange = new vscode.EventEmitter<void>()
28+
public onDidChangeInlayHints = this.onDidChange.event
29+
30+
constructor (private client: MonacoLanguageClient) {
31+
this.client.onRequest(InlayHintRefreshRequest.type, async () => {
32+
this.onDidChange.fire()
33+
})
34+
}
35+
36+
public async provideInlayHints (document: vscode.TextDocument, range: vscode.Range, token: vscode.CancellationToken): Promise<vscode.InlayHint[] | undefined> {
37+
const requestParams: InlayHintParams = {
38+
textDocument: this.client.code2ProtocolConverter.asTextDocumentIdentifier(document),
39+
range: this.client.code2ProtocolConverter.asRange(range)
40+
}
41+
try {
42+
const values = await this.client.sendRequest(InlayHintRequest.type, requestParams, token)
43+
if (token.isCancellationRequested) {
44+
return []
45+
}
46+
return asInlayHints(values, this.client)
47+
} catch (error) {
48+
return this.client.handleFailedRequest(InlayHintRequest.type, token, error, [])
49+
}
50+
}
51+
}
52+
53+
export class JavaExtensionFeature implements StaticFeature {
54+
private disposables: DisposableCollection
55+
constructor (private languageClient: MonacoLanguageClient) {
56+
this.disposables = new DisposableCollection()
57+
}
58+
59+
fillClientCapabilities (): void {}
60+
61+
initialize (): void {
62+
// Comes from https://github.com/redhat-developer/vscode-java/blob/9b6046eecc65fd47507f309a3ccc9add45c6d3be/src/standardLanguageClient.ts#L321
63+
this.disposables.push(vscode.commands.registerCommand('java.apply.workspaceEdit', async (obj: WorkspaceEdit) => {
64+
const edit = await this.languageClient.protocol2CodeConverter.asWorkspaceEdit(obj)
65+
return vscode.workspace.applyEdit(edit)
66+
}))
67+
68+
this.disposables.push(vscode.languages.registerInlayHintsProvider(this.languageClient.clientOptions.documentSelector!, new JavaInlayHintsProvider(this.languageClient)))
69+
}
70+
71+
getState (): FeatureState {
72+
return {
73+
kind: 'static'
74+
}
75+
}
76+
77+
dispose (): void {
78+
this.disposables.dispose()
79+
}
80+
}

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@ import { WillShutdownParams } from './customRequests'
66
import { loadExtensionConfigurations } from './extensionConfiguration'
77
import './hacks'
88
import { createLanguageClientManager, LanguageClientManager, LanguageClientManagerOptions, StatusChangeEvent } from './languageClient'
9-
import { LanguageClientId, LanguageClientOptions, registerLanguageClient } from './languageClientOptions'
9+
import { getLanguageClientOptions, LanguageClientId, LanguageClientOptions, registerLanguageClient } from './languageClientOptions'
1010
import { StaticLanguageClientId } from './staticOptions'
1111

1212
export {
1313
loadExtensionConfigurations,
1414
createLanguageClientManager,
1515
registerLanguageClient,
16+
getLanguageClientOptions,
1617
LanguageClientManager,
1718
CodinGameInfrastructure
1819
}

src/languageClientOptions.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { Disposable, LanguageClientOptions as MonacoLanguageClientOptions } from 'monaco-languageclient'
1+
import { Disposable, LanguageClientOptions as MonacoLanguageClientOptions, MonacoLanguageClient } from 'monaco-languageclient'
2+
import { StaticFeature, DynamicFeature } from 'vscode-languageclient/lib/common/api'
23
import staticOptions, { StaticLanguageClientId } from './staticOptions'
34

45
export type LanguageClientOptions = Pick<MonacoLanguageClientOptions, 'documentSelector' | 'synchronize' | 'initializationOptions' | 'middleware'> & {
@@ -17,6 +18,8 @@ export type LanguageClientOptions = Pick<MonacoLanguageClientOptions, 'documentS
1718
* The language server will only be considered ready after this log message was received
1819
*/
1920
readinessMessageMatcher?: RegExp
21+
22+
createAdditionalFeatures?(client: MonacoLanguageClient): (StaticFeature | DynamicFeature<unknown>)[]
2023
}
2124

2225
const dynamicOptions: Partial<Record<string, LanguageClientOptions>> = {}

src/services.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11

22
import { Services } from 'vscode/services'
3-
import { WorkspaceEdit, Disposable } from 'vscode-languageserver-protocol'
4-
import * as vscode from 'vscode'
5-
import { createConverter as createProtocolConverter } from 'vscode-languageclient/lib/common/protocolConverter'
63
import WatchableConsoleWindow from './services/WatchableConsoleWindow'
74
import CodinGameMonacoWorkspace from './services/CodinGameMonacoWorkspace'
85
import { Infrastructure } from './infrastructure'
@@ -13,25 +10,13 @@ interface CgMonacoServices extends Services {
1310
window: WatchableConsoleWindow
1411
}
1512

16-
function installCommands (): Disposable {
17-
// Comes from https://github.com/redhat-developer/vscode-java/blob/9b0f0aca80cbefabad4c034fb5dd365d029f6170/src/extension.ts#L155-L160
18-
// Other commands needs to be implemented as well?
19-
// (https://github.com/eclipse/eclipse.jdt.ls/issues/376#issuecomment-333923685)
20-
const protocolConverter = createProtocolConverter(undefined, true, true)
21-
return vscode.commands.registerCommand('java.apply.workspaceEdit', async (obj: WorkspaceEdit) => {
22-
const edit = await protocolConverter.asWorkspaceEdit(obj)
23-
return vscode.workspace.applyEdit(edit)
24-
})
25-
}
26-
2713
const services = {
2814
workspace: new CodinGameMonacoWorkspace('file:///tmp/project'),
2915
window: new WatchableConsoleWindow(),
3016
env: new CodinGameMonacoEnv()
3117
}
3218

3319
Services.install(services)
34-
installCommands()
3520

3621
function updateServices (infrastructure: Infrastructure): void {
3722
services.workspace.initialize(

src/services/WatchableConsoleWindow.ts

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,35 @@ class WatchableOutputChannel implements vscode.OutputChannel {
5252
}
5353
}
5454

55+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
56+
function isMessageItem (item: any): item is vscode.MessageItem {
57+
return item != null && item.title
58+
}
59+
60+
function getMessage (item: string | vscode.MessageItem) {
61+
return isMessageItem(item) ? item.title : item
62+
}
63+
5564
export default class WatchableConsoleWindow implements Window {
5665
protected readonly onDidChangeChannelsEmitter = new monaco.Emitter<void>()
5766
protected readonly channels = new Map<string, WatchableOutputChannel>()
5867

5968
async showMessage<T extends vscode.MessageOptions | string | vscode.MessageItem> (type: Severity, message: string, ...actions: T[]): Promise<T | undefined> {
60-
const displayedMessage = message + '\n' + actions.map(action => `- ${(action as vscode.MessageItem).title}`).join('\n')
69+
const optionsOrFirstItem = actions[0]
70+
let items: (string | vscode.MessageItem)[]
71+
72+
let options: vscode.MessageOptions | undefined
73+
if (typeof optionsOrFirstItem === 'string' || isMessageItem(optionsOrFirstItem)) {
74+
items = actions as (string | vscode.MessageItem)[]
75+
} else {
76+
options = optionsOrFirstItem
77+
items = actions.slice(1) as (string | vscode.MessageItem)[]
78+
}
79+
80+
let displayedMessage = message
81+
if (items.length > 0) {
82+
displayedMessage = `${displayedMessage}\nActions:\n${items.map(action => `- ${getMessage(action)}`).join('\n')}`
83+
}
6184

6285
if (type === Severity.Error) {
6386
console.error('[LSP]', displayedMessage)
@@ -69,20 +92,25 @@ export default class WatchableConsoleWindow implements Window {
6992
console.info('[LSP]', displayedMessage)
7093
}
7194

72-
if (actions.length > 1) {
73-
return swal({
74-
text: message,
75-
buttons: actions.reduce((acc, action, index) => ({
95+
const defaultAction = items.find(item => isMessageItem(item) && (item.isCloseAffordance ?? false)) as T | undefined ?? actions[0]
96+
97+
if (items.length > 1) {
98+
return (await swal({
99+
title: message,
100+
text: options?.detail,
101+
closeOnEsc: true,
102+
closeOnClickOutside: true,
103+
buttons: items.reduce((acc, action, index) => ({
76104
...acc,
77105
[`option-${index}`]: {
78-
text: (action as vscode.MessageItem).title,
106+
text: getMessage(action),
79107
value: action
80108
}
81109
}), {})
82-
})
110+
})) ?? defaultAction
83111
}
84112

85-
return actions[0]
113+
return defaultAction
86114
}
87115

88116
createOutputChannel (name: string): vscode.OutputChannel {

0 commit comments

Comments
 (0)