Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lsp): LSP 3.18 #5126

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 27 additions & 22 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
"eslint-plugin-jsdoc": "^48.11.0",
"jest": "29.7.0",
"typescript": "^5.5.4",
"vscode-languageserver": "^9.0.1"
"vscode-languageserver": "^10.0.0-next.11"
},
"dependencies": {
"@chemzqm/msgpack-lite": "^0.1.29",
Expand All @@ -108,9 +108,9 @@
"unidecode": "^1.0.1",
"unzip-stream": "^0.3.4",
"uuid": "^9.0.1",
"vscode-languageserver-protocol": "^3.17.5",
"vscode-languageserver-textdocument": "^1.0.11",
"vscode-languageserver-types": "^3.17.5",
"vscode-languageserver-protocol": "^3.17.6-next.11",
"vscode-languageserver-textdocument": "^1.0.12",
"vscode-languageserver-types": "^3.17.6-next.5",
"vscode-uri": "^3.0.8",
"which": "^4.0.0"
}
Expand Down
126 changes: 119 additions & 7 deletions src/__tests__/client/features.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as assert from 'assert'
import path from 'path'
import { CallHierarchyIncomingCall, CallHierarchyItem, CallHierarchyOutgoingCall, CallHierarchyPrepareRequest, CancellationToken, CancellationTokenSource, CodeAction, CodeActionRequest, CodeLensRequest, Color, ColorInformation, ColorPresentation, CompletionItem, CompletionRequest, CompletionTriggerKind, ConfigurationRequest, DeclarationRequest, DefinitionRequest, DidChangeConfigurationNotification, DidChangeTextDocumentNotification, DidChangeWatchedFilesNotification, DidCloseTextDocumentNotification, DidCreateFilesNotification, DidDeleteFilesNotification, DidOpenTextDocumentNotification, DidRenameFilesNotification, DidSaveTextDocumentNotification, Disposable, DocumentColorRequest, DocumentDiagnosticReport, DocumentDiagnosticReportKind, DocumentDiagnosticRequest, DocumentFormattingRequest, DocumentHighlight, DocumentHighlightKind, DocumentHighlightRequest, DocumentLink, DocumentLinkRequest, DocumentOnTypeFormattingRequest, DocumentRangeFormattingRequest, DocumentSelector, DocumentSymbolRequest, FoldingRange, FoldingRangeRequest, FullDocumentDiagnosticReport, Hover, HoverRequest, ImplementationRequest, InlayHintKind, InlayHintLabelPart, InlayHintRequest, InlineValueEvaluatableExpression, InlineValueRequest, InlineValueText, InlineValueVariableLookup, LinkedEditingRangeRequest, Location, NotificationType0, ParameterInformation, Position, ProgressToken, ProtocolRequestType, Range, ReferencesRequest, RenameRequest, SelectionRange, SelectionRangeRequest, SemanticTokensRegistrationType, SignatureHelpRequest, SignatureHelpTriggerKind, SignatureInformation, TextDocumentEdit, TextDocumentSyncKind, TextEdit, TypeDefinitionRequest, TypeHierarchyPrepareRequest, WillCreateFilesRequest, WillDeleteFilesRequest, WillRenameFilesRequest, WillSaveTextDocumentNotification, WillSaveTextDocumentWaitUntilRequest, WorkDoneProgressBegin, WorkDoneProgressCreateRequest, WorkDoneProgressEnd, WorkDoneProgressReport, WorkspaceEdit, WorkspaceSymbolRequest } from 'vscode-languageserver-protocol'
import { ApplyWorkspaceEditParams, CallHierarchyIncomingCall, CallHierarchyItem, CallHierarchyOutgoingCall, CallHierarchyPrepareRequest, CancellationToken, CancellationTokenSource, CodeAction, CodeActionRequest, CodeLensRequest, Color, ColorInformation, ColorPresentation, CompletionItem, CompletionRequest, CompletionTriggerKind, ConfigurationRequest, DeclarationRequest, DefinitionRequest, DidChangeConfigurationNotification, DidChangeTextDocumentNotification, DidChangeWatchedFilesNotification, DidCloseTextDocumentNotification, DidCreateFilesNotification, DidDeleteFilesNotification, DidOpenTextDocumentNotification, DidRenameFilesNotification, DidSaveTextDocumentNotification, Disposable, DocumentColorRequest, DocumentDiagnosticReport, DocumentDiagnosticReportKind, DocumentDiagnosticRequest, DocumentFormattingRequest, DocumentHighlight, DocumentHighlightKind, DocumentHighlightRequest, DocumentLink, DocumentLinkRequest, DocumentOnTypeFormattingRequest, DocumentRangeFormattingRequest, DocumentSelector, DocumentSymbolRequest, FoldingRange, FoldingRangeRequest, FullDocumentDiagnosticReport, Hover, HoverRequest, ImplementationRequest, InlayHintKind, InlayHintLabelPart, InlayHintRequest, InlineCompletionItem, InlineCompletionRequest, InlineValueEvaluatableExpression, InlineValueRequest, InlineValueText, InlineValueVariableLookup, LinkedEditingRangeRequest, Location, NotificationType0, ParameterInformation, Position, ProgressToken, ProtocolRequestType, Range, ReferencesRequest, RenameRequest, SelectionRange, SelectionRangeRequest, SemanticTokensRegistrationType, SignatureHelpRequest, SignatureHelpTriggerKind, SignatureInformation, TextDocumentEdit, TextDocumentSyncKind, TextEdit, TypeDefinitionRequest, TypeHierarchyPrepareRequest, WillCreateFilesRequest, WillDeleteFilesRequest, WillRenameFilesRequest, WillSaveTextDocumentNotification, WillSaveTextDocumentWaitUntilRequest, WorkDoneProgressBegin, WorkDoneProgressCreateRequest, WorkDoneProgressEnd, WorkDoneProgressReport, WorkspaceEdit, WorkspaceSymbolRequest } from 'vscode-languageserver-protocol'
import { TextDocument } from 'vscode-languageserver-textdocument'
import { URI } from 'vscode-uri'
import commands from '../../commands'
Expand Down Expand Up @@ -97,11 +97,12 @@ describe('Client integration', () => {
run: { module: serverModule, transport: TransportKind.ipc },
debug: { module: serverModule, transport: TransportKind.ipc, options: { execArgv: ['--nolazy', '--inspect=6014'] } }
}
const documentSelector: DocumentSelector = [{ scheme: 'lsptests' }]
const documentSelector: DocumentSelector = [{ scheme: 'lsptests', language: 'bat' }]

middleware = {}
const clientOptions: LanguageClientOptions = {
documentSelector, synchronize: {}, initializationOptions: {}, middleware
documentSelector, synchronize: {}, initializationOptions: {}, middleware,
workspaceFolder: { name: 'test_folder', uri: URI.parse('file-test:///').toString() },
}

client = new LanguageClient('test svr', 'Test Language Server', serverOptions, clientOptions)
Expand Down Expand Up @@ -137,7 +138,9 @@ describe('Client integration', () => {
resolveProvider: true
},
documentFormattingProvider: true,
documentRangeFormattingProvider: true,
documentRangeFormattingProvider: {
rangesSupport: true
},
documentOnTypeFormattingProvider: {
firstTriggerCharacter: ':'
},
Expand All @@ -154,6 +157,7 @@ describe('Client integration', () => {
documentSelector: [{ language: '*' }]
},
selectionRangeProvider: true,
inlineCompletionProvider: true,
inlineValueProvider: {},
inlayHintProvider: {
resolveProvider: true
Expand Down Expand Up @@ -270,6 +274,7 @@ describe('Client integration', () => {
testFeature(SemanticTokensRegistrationType.method, 'document')
testFeature(LinkedEditingRangeRequest.method, 'document')
testFeature(TypeHierarchyPrepareRequest.method, 'document')
testFeature(InlineCompletionRequest.method, 'document')
testFeature(InlineValueRequest.method, 'document')
testFeature(InlayHintRequest.method, 'document')
testFeature(WorkspaceSymbolRequest.method, 'workspace')
Expand Down Expand Up @@ -538,6 +543,40 @@ describe('Client integration', () => {
)
})

test('Progress percentage is an integer', async () => {
const progressToken = 'TEST-PROGRESS-PERCENTAGE'
const percentages: Array<number | undefined> = []
let currentProgressResolver: (value: unknown) => void | undefined

// Set up middleware that calls the current resolve function when it gets its 'end' progress event.
middleware.handleWorkDoneProgress = (token: ProgressToken, params: WorkDoneProgressBegin | WorkDoneProgressReport | WorkDoneProgressEnd, next) => {
if (token === progressToken) {
const percentage = params.kind === 'report' || params.kind === 'begin' ? params.percentage : undefined
percentages.push(percentage)

if (params.kind === 'end') {
setImmediate(currentProgressResolver)
}
}
return next(token, params)
}

// Trigger a progress event.
await new Promise<unknown>(resolve => {
currentProgressResolver = resolve
void client.sendRequest(
new ProtocolRequestType<any, null, never, any, any>('testing/sendPercentageProgress'),
{},
tokenSource.token,
)
})

middleware.handleWorkDoneProgress = undefined

// Ensure percentages are rounded according to the spec
assert.deepStrictEqual(percentages, [0, 50, undefined])
})

test('Document Formatting', async () => {
const provider = client.getFeature(DocumentFormattingRequest.method).getProvider(document)
isDefined(provider)
Expand Down Expand Up @@ -822,7 +861,7 @@ describe('Client integration', () => {

const referenceFileUri = URI.parse('/dummy-edit')
function ensureReferenceEdit(edits: WorkspaceEdit, type: string, expectedLines: string[]) {
// // Ensure the edits are as expected.
// Ensure the edits are as expected.
assert.strictEqual(edits.documentChanges?.length, 1)
const edit = edits.documentChanges[0] as TextDocumentEdit
assert.strictEqual(edit.edits.length, 1)
Expand Down Expand Up @@ -1351,6 +1390,27 @@ describe('Client integration', () => {
}, true)
})

test('Inline Completions', async () => {
const provider = client.getFeature(InlineCompletionRequest.method)?.getProvider(document)
isDefined(provider)
const results = (await provider.provideInlineCompletionItems(document, position, { triggerKind: 1, selectedCompletionInfo: { range, text: 'text' } }, tokenSource.token)) as InlineCompletionItem[]

isArray(results, InlineCompletionItem, 1)

rangeEqual(results[0].range!, 1, 2, 3, 4)
assert.strictEqual(results[0].filterText!, 'te')
assert.strictEqual(results[0].insertText, 'text inline')

let middlewareCalled = false
middleware.provideInlineCompletionItems = (d, r, c, t, n) => {
middlewareCalled = true
return n(d, r, c, t)
}
await provider.provideInlineCompletionItems(document, position, { triggerKind: 1, selectedCompletionInfo: undefined }, tokenSource.token)
middleware.provideInlineCompletionItems = undefined
assert.strictEqual(middlewareCalled, true)
})

test('Workspace symbols', async () => {
const providers = client.getFeature(WorkspaceSymbolRequest.method).getProviders()
isDefined(providers)
Expand All @@ -1365,6 +1425,58 @@ describe('Client integration', () => {
isDefined(symbol)
rangeEqual(symbol.location['range'], 1, 2, 3, 4)
})

test('General middleware', async () => {
let middlewareCallCount = 0
// Add a general middleware for both requests and notifications
middleware.sendRequest = (type, param, token, next) => {
middlewareCallCount++
return next(type, param, token)
}
middleware.sendNotification = (type, next, params) => {
middlewareCallCount++
return next(type, params)
}
// Send a request
const definitionProvider = client.getFeature(DefinitionRequest.method).getProvider(document)
isDefined(definitionProvider)
await definitionProvider.provideDefinition(document, position, tokenSource.token)
// Send a notification
const notificationProvider = client.getFeature(DidSaveTextDocumentNotification.method).getProvider(document)
isDefined(notificationProvider)
await notificationProvider.send(document)
// Verify that both the request and notification went through the middleware
middleware.sendRequest = undefined
middleware.sendNotification = undefined
assert.strictEqual(middlewareCallCount, 2)
})

test('applyEdit middleware', async () => {
const middlewareEvents: Array<ApplyWorkspaceEditParams> = []
let currentProgressResolver: (value: unknown) => void | undefined

middleware.workspace = middleware.workspace || {}
middleware.workspace.handleApplyEdit = async (params, next) => {
middlewareEvents.push(params)
setImmediate(currentProgressResolver)
return next(params, tokenSource.token)
}

// Trigger sample applyEdit event.
await new Promise<unknown>(resolve => {
currentProgressResolver = resolve
void client.sendRequest(
new ProtocolRequestType<any, null, never, any, any>('testing/sendApplyEdit'),
{},
tokenSource.token,
)
})

middleware.workspace.handleApplyEdit = undefined

// Ensure event was handled.
assert.deepStrictEqual(middlewareEvents, [{ label: 'Apply Edit', edit: {} }])
})
})

namespace CrashNotification {
Expand All @@ -1383,9 +1495,9 @@ class CrashClient extends LanguageClient {
})
}

protected handleConnectionClosed(): void {
super.handleConnectionClosed()
protected handleConnectionClosed(): Promise<void> {
this.resolve!()
return super.handleConnectionClosed()
}
}

Expand Down
20 changes: 10 additions & 10 deletions src/__tests__/client/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import os from 'os'
import { v4 as uuid } from 'uuid'
import { CancellationToken, CancellationTokenSource, DidCreateFilesNotification, Disposable, ErrorCodes, InlayHintRequest, LSPErrorCodes, MessageType, ResponseError, Trace, WorkDoneProgress } from 'vscode-languageserver-protocol'
import { IPCMessageReader, IPCMessageWriter } from 'vscode-languageserver-protocol/node'
import { Diagnostic, MarkupKind, Range } from 'vscode-languageserver-types'
import { MarkupKind, Range } from 'vscode-languageserver-types'
import { URI } from 'vscode-uri'
import * as lsclient from '../../language-client'
import { CloseAction, ErrorAction, HandleDiagnosticsSignature } from '../../language-client'
import { CloseAction, ErrorAction } from '../../language-client'
import { LSPCancellationError } from '../../language-client/features'
import { InitializationFailedHandler } from '../../language-client/utils/errorHandler'
import { disposeAll } from '../../util'
Expand Down Expand Up @@ -213,11 +213,11 @@ describe('Client events', () => {
synchronize: {},
errorHandler: {
error: () => {
return ErrorAction.Shutdown
return { action: ErrorAction.Shutdown }
},
closed: () => {
called = true
return CloseAction.DoNotRestart
return { action: CloseAction.DoNotRestart }
}
},
initializationOptions: { initEvent: true }
Expand All @@ -234,7 +234,7 @@ describe('Client events', () => {
await helper.waitValue(() => {
return called
}, true)
client.handleConnectionError(new Error('error'), { jsonrpc: '' }, 1)
void client.handleConnectionError(new Error('error'), { jsonrpc: '' }, 1)
})

it('should handle message events', async () => {
Expand Down Expand Up @@ -295,9 +295,9 @@ describe('Client events', () => {
synchronize: {},
middleware: {
window: {
showDocument: async (params, next) => {
showDocument: async (params, token, next) => {
called = true
let res = await next(params, CancellationToken.None)
let res = await next(params, token)
return res as any
}
}
Expand Down Expand Up @@ -437,11 +437,11 @@ describe('Client integration', () => {
},
stdioEncoding: 'utf8',
errorHandler: {
error: (): lsclient.ErrorAction => {
return lsclient.ErrorAction.Continue
error: () => {
return { action: lsclient.ErrorAction.Continue }
},
closed: () => {
return lsclient.CloseAction.DoNotRestart
return { action: lsclient.CloseAction.DoNotRestart }
}
},
progressOnInitialization: true,
Expand Down
Loading