diff --git a/src/harness/client.ts b/src/harness/client.ts index 4f11c9d1a9..e97a87be8b 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -1,835 +1,835 @@ -namespace ts.server { - export interface SessionClientHost extends LanguageServiceHost { - writeMessage(message: string): void; - } - - interface RenameEntry { - readonly renameInfo: RenameInfo; - readonly inputs: { - readonly fileName: string; - readonly position: number; - readonly findInStrings: boolean; - readonly findInComments: boolean; - }; - readonly locations: RenameLocation[]; - } - - /* @internal */ - export function extractMessage(message: string): string { - // Read the content length - const contentLengthPrefix = "Content-Length: "; - const lines = message.split(/\r?\n/); - Debug.assert(lines.length >= 2, "Malformed response: Expected 3 lines in the response."); - - const contentLengthText = lines[0]; - Debug.assert(contentLengthText.indexOf(contentLengthPrefix) === 0, "Malformed response: Response text did not contain content-length header."); - const contentLength = parseInt(contentLengthText.substring(contentLengthPrefix.length)); - - // Read the body - const responseBody = lines[2]; - - // Verify content length - Debug.assert(responseBody.length + 1 === contentLength, "Malformed response: Content length did not match the response's body length."); - return responseBody; - } - - export class SessionClient implements LanguageService { - private sequence = 0; - private lineMaps: Map = createMap(); - private messages: string[] = []; - private lastRenameEntry: RenameEntry | undefined; - - constructor(private host: SessionClientHost) { - } - - public onMessage(message: string): void { - this.messages.push(message); - } - - private writeMessage(message: string): void { - this.host.writeMessage(message); - } - - private getLineMap(fileName: string): number[] { - let lineMap = this.lineMaps.get(fileName); - if (!lineMap) { - lineMap = computeLineStarts(getSnapshotText(this.host.getScriptSnapshot(fileName)!)); - this.lineMaps.set(fileName, lineMap); - } - return lineMap; - } - - private lineOffsetToPosition(fileName: string, lineOffset: protocol.Location, lineMap?: number[]): number { - lineMap = lineMap || this.getLineMap(fileName); - return computePositionOfLineAndCharacter(lineMap, lineOffset.line - 1, lineOffset.offset - 1); - } - - private positionToOneBasedLineOffset(fileName: string, position: number): protocol.Location { - const lineOffset = computeLineAndCharacterOfPosition(this.getLineMap(fileName), position); - return { - line: lineOffset.line + 1, - offset: lineOffset.character + 1 - }; - } - - private convertCodeEditsToTextChange(fileName: string, codeEdit: protocol.CodeEdit): TextChange { - return { span: this.decodeSpan(codeEdit, fileName), newText: codeEdit.newText }; - } - - private processRequest(command: string, args: T["arguments"]): T { - const request: protocol.Request = { - seq: this.sequence, - type: "request", - arguments: args, - command - }; - this.sequence++; - - this.writeMessage(JSON.stringify(request)); - - return request; - } - - private processResponse(request: protocol.Request, expectEmptyBody = false): T { - let foundResponseMessage = false; - let response!: T; - while (!foundResponseMessage) { - const lastMessage = this.messages.shift()!; - Debug.assert(!!lastMessage, "Did not receive any responses."); - const responseBody = extractMessage(lastMessage); - try { - response = JSON.parse(responseBody); - // the server may emit events before emitting the response. We - // want to ignore these events for testing purpose. - if (response.type === "response") { - foundResponseMessage = true; - } - } - catch (e) { - throw new Error("Malformed response: Failed to parse server response: " + lastMessage + ". \r\n Error details: " + e.message); - } - } - - // verify the sequence numbers - Debug.assert(response.request_seq === request.seq, "Malformed response: response sequence number did not match request sequence number."); - - // unmarshal errors - if (!response.success) { - throw new Error("Error " + response.message); - } - - Debug.assert(expectEmptyBody || !!response.body, "Malformed response: Unexpected empty response body."); - Debug.assert(!expectEmptyBody || !response.body, "Malformed response: Unexpected non-empty response body."); - - return response; - } - - /*@internal*/ - configure(preferences: UserPreferences) { - const args: protocol.ConfigureRequestArguments = { preferences }; - const request = this.processRequest(CommandNames.Configure, args); - this.processResponse(request, /*expectEmptyBody*/ true); - } - - openFile(file: string, fileContent?: string, scriptKindName?: "TS" | "JS" | "TSX" | "JSX"): void { - const args: protocol.OpenRequestArgs = { file, fileContent, scriptKindName }; - this.processRequest(CommandNames.Open, args); - } - - closeFile(file: string): void { - const args: protocol.FileRequestArgs = { file }; - this.processRequest(CommandNames.Close, args); - } - - createChangeFileRequestArgs(fileName: string, start: number, end: number, insertString: string): protocol.ChangeRequestArgs { - return { ...this.createFileLocationRequestArgsWithEndLineAndOffset(fileName, start, end), insertString }; - } - - changeFile(fileName: string, args: protocol.ChangeRequestArgs): void { - // clear the line map after an edit - this.lineMaps.set(fileName, undefined!); // TODO: GH#18217 - this.processRequest(CommandNames.Change, args); - } - - toLineColumnOffset(fileName: string, position: number) { - const { line, offset } = this.positionToOneBasedLineOffset(fileName, position); - return { line, character: offset }; - } - - getQuickInfoAtPosition(fileName: string, position: number): QuickInfo { - const args = this.createFileLocationRequestArgs(fileName, position); - - const request = this.processRequest(CommandNames.Quickinfo, args); - const response = this.processResponse(request); - const body = response.body!; // TODO: GH#18217 - - return { - kind: body.kind, - kindModifiers: body.kindModifiers, - textSpan: this.decodeSpan(body, fileName), - displayParts: [{ kind: "text", text: body.displayString }], - documentation: [{ kind: "text", text: body.documentation }], - tags: body.tags - }; - } - - getProjectInfo(file: string, needFileNameList: boolean): protocol.ProjectInfo { - const args: protocol.ProjectInfoRequestArgs = { file, needFileNameList }; - - const request = this.processRequest(CommandNames.ProjectInfo, args); - const response = this.processResponse(request); - - return { - configFileName: response.body!.configFileName, // TODO: GH#18217 - fileNames: response.body!.fileNames - }; - } - - getCompletionsAtPosition(fileName: string, position: number, _preferences: UserPreferences | undefined): CompletionInfo { - // Not passing along 'preferences' because server should already have those from the 'configure' command - const args: protocol.CompletionsRequestArgs = this.createFileLocationRequestArgs(fileName, position); - - const request = this.processRequest(CommandNames.Completions, args); - const response = this.processResponse(request); - - return { - isGlobalCompletion: false, - isMemberCompletion: false, - isNewIdentifierLocation: false, - entries: response.body!.map(entry => { // TODO: GH#18217 - if (entry.replacementSpan !== undefined) { - const { name, kind, kindModifiers, sortText, replacementSpan, hasAction, source, isRecommended } = entry; - // TODO: GH#241 - const res: CompletionEntry = { name, kind, kindModifiers, sortText, replacementSpan: this.decodeSpan(replacementSpan, fileName), hasAction, source, isRecommended }; - return res; - } - - return entry as { name: string, kind: ScriptElementKind, kindModifiers: string, sortText: string }; // TODO: GH#18217 - }) - }; - } - - getCompletionEntryDetails(fileName: string, position: number, entryName: string, _options: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined): CompletionEntryDetails { - const args: protocol.CompletionDetailsRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), entryNames: [{ name: entryName, source }] }; - - const request = this.processRequest(CommandNames.CompletionDetails, args); - const response = this.processResponse(request); - Debug.assert(response.body!.length === 1, "Unexpected length of completion details response body."); - const convertedCodeActions = map(response.body![0].codeActions, ({ description, changes }) => ({ description, changes: this.convertChanges(changes, fileName) })); - return { ...response.body![0], codeActions: convertedCodeActions }; - } - - getCompletionEntrySymbol(_fileName: string, _position: number, _entryName: string): Symbol { - return notImplemented(); - } - - getNavigateToItems(searchValue: string): NavigateToItem[] { - const args: protocol.NavtoRequestArgs = { - searchValue, - file: this.host.getScriptFileNames()[0] - }; - - const request = this.processRequest(CommandNames.Navto, args); - const response = this.processResponse(request); - - return response.body!.map(entry => ({ // TODO: GH#18217 - name: entry.name, - containerName: entry.containerName || "", - containerKind: entry.containerKind || ScriptElementKind.unknown, - kind: entry.kind, - kindModifiers: entry.kindModifiers || "", - matchKind: entry.matchKind as keyof typeof PatternMatchKind, - isCaseSensitive: entry.isCaseSensitive, - fileName: entry.file, - textSpan: this.decodeSpan(entry), - })); - } - - getFormattingEditsForRange(file: string, start: number, end: number, _options: FormatCodeOptions): TextChange[] { - const args: protocol.FormatRequestArgs = this.createFileLocationRequestArgsWithEndLineAndOffset(file, start, end); - - - // TODO: handle FormatCodeOptions - const request = this.processRequest(CommandNames.Format, args); - const response = this.processResponse(request); - - return response.body!.map(entry => this.convertCodeEditsToTextChange(file, entry)); // TODO: GH#18217 - } - - getFormattingEditsForDocument(fileName: string, options: FormatCodeOptions): TextChange[] { - return this.getFormattingEditsForRange(fileName, 0, this.host.getScriptSnapshot(fileName)!.getLength(), options); - } - - getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, _options: FormatCodeOptions): TextChange[] { - const args: protocol.FormatOnKeyRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), key }; - - // TODO: handle FormatCodeOptions - const request = this.processRequest(CommandNames.Formatonkey, args); - const response = this.processResponse(request); - - return response.body!.map(entry => this.convertCodeEditsToTextChange(fileName, entry)); // TODO: GH#18217 - } - - getDefinitionAtPosition(fileName: string, position: number): DefinitionInfo[] { - const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position); - - const request = this.processRequest(CommandNames.Definition, args); - const response = this.processResponse(request); - - return response.body!.map(entry => ({ // TODO: GH#18217 - containerKind: ScriptElementKind.unknown, - containerName: "", - fileName: entry.file, - textSpan: this.decodeSpan(entry), - kind: ScriptElementKind.unknown, - name: "" - })); - } - - getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan { - const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position); - - const request = this.processRequest(CommandNames.DefinitionAndBoundSpan, args); - const response = this.processResponse(request); - const body = Debug.checkDefined(response.body); // TODO: GH#18217 - - return { - definitions: body.definitions.map(entry => ({ - containerKind: ScriptElementKind.unknown, - containerName: "", - fileName: entry.file, - textSpan: this.decodeSpan(entry), - kind: ScriptElementKind.unknown, - name: "" - })), - textSpan: this.decodeSpan(body.textSpan, request.arguments.file) - }; - } - - getTypeDefinitionAtPosition(fileName: string, position: number): DefinitionInfo[] { - const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position); - - const request = this.processRequest(CommandNames.TypeDefinition, args); - const response = this.processResponse(request); - - return response.body!.map(entry => ({ // TODO: GH#18217 - containerKind: ScriptElementKind.unknown, - containerName: "", - fileName: entry.file, - textSpan: this.decodeSpan(entry), - kind: ScriptElementKind.unknown, - name: "" - })); - } - - getImplementationAtPosition(fileName: string, position: number): ImplementationLocation[] { - const args = this.createFileLocationRequestArgs(fileName, position); - - const request = this.processRequest(CommandNames.Implementation, args); - const response = this.processResponse(request); - - return response.body!.map(entry => ({ // TODO: GH#18217 - fileName: entry.file, - textSpan: this.decodeSpan(entry), - kind: ScriptElementKind.unknown, - displayParts: [] - })); - } - - findReferences(_fileName: string, _position: number): ReferencedSymbol[] { - // Not yet implemented. - return []; - } - - getReferencesAtPosition(fileName: string, position: number): ReferenceEntry[] { - const args = this.createFileLocationRequestArgs(fileName, position); - - const request = this.processRequest(CommandNames.References, args); - const response = this.processResponse(request); - - return response.body!.refs.map(entry => ({ // TODO: GH#18217 - fileName: entry.file, - textSpan: this.decodeSpan(entry), - isWriteAccess: entry.isWriteAccess, - isDefinition: entry.isDefinition, - })); - } - - getEmitOutput(file: string): EmitOutput { - const request = this.processRequest(protocol.CommandTypes.EmitOutput, { file }); - const response = this.processResponse(request); - return response.body as EmitOutput; - } - - getSyntacticDiagnostics(file: string): DiagnosticWithLocation[] { - return this.getDiagnostics(file, CommandNames.SyntacticDiagnosticsSync); - } - getSemanticDiagnostics(file: string): Diagnostic[] { - return this.getDiagnostics(file, CommandNames.SemanticDiagnosticsSync); - } - getSuggestionDiagnostics(file: string): DiagnosticWithLocation[] { - return this.getDiagnostics(file, CommandNames.SuggestionDiagnosticsSync); - } - - private getDiagnostics(file: string, command: CommandNames): DiagnosticWithLocation[] { - const request = this.processRequest(command, { file, includeLinePosition: true }); - const response = this.processResponse(request); - const sourceText = getSnapshotText(this.host.getScriptSnapshot(file)!); - const fakeSourceFile = { fileName: file, text: sourceText } as SourceFile; // Warning! This is a huge lie! - - return (response.body).map((entry): DiagnosticWithLocation => { - const category = firstDefined(Object.keys(DiagnosticCategory), id => - isString(id) && entry.category === id.toLowerCase() ? (DiagnosticCategory)[id] : undefined); - return { - file: fakeSourceFile, - start: entry.start, - length: entry.length, - messageText: entry.message, - category: Debug.checkDefined(category, "convertDiagnostic: category should not be undefined"), - code: entry.code, - reportsUnnecessary: entry.reportsUnnecessary, - }; - }); - } - - getCompilerOptionsDiagnostics(): Diagnostic[] { - return notImplemented(); - } - - getRenameInfo(fileName: string, position: number, _options?: RenameInfoOptions, findInStrings?: boolean, findInComments?: boolean): RenameInfo { - // Not passing along 'options' because server should already have those from the 'configure' command - const args: protocol.RenameRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), findInStrings, findInComments }; - - const request = this.processRequest(CommandNames.Rename, args); - const response = this.processResponse(request); - const body = response.body!; // TODO: GH#18217 - const locations: RenameLocation[] = []; - for (const entry of body.locs) { - const fileName = entry.file; - for (const { start, end, contextStart, contextEnd, ...prefixSuffixText } of entry.locs) { - locations.push({ - textSpan: this.decodeSpan({ start, end }, fileName), - fileName, - ...(contextStart !== undefined ? - { contextSpan: this.decodeSpan({ start: contextStart, end: contextEnd! }, fileName) } : - undefined), - ...prefixSuffixText - }); - } - } - - const renameInfo = body.info.canRename - ? identity({ - canRename: body.info.canRename, - fileToRename: body.info.fileToRename, - displayName: body.info.displayName, - fullDisplayName: body.info.fullDisplayName, - kind: body.info.kind, - kindModifiers: body.info.kindModifiers, - triggerSpan: createTextSpanFromBounds(position, position), - }) - : identity({ canRename: false, localizedErrorMessage: body.info.localizedErrorMessage }); - this.lastRenameEntry = { - renameInfo, - inputs: { - fileName, - position, - findInStrings: !!findInStrings, - findInComments: !!findInComments, - }, - locations, - }; - return renameInfo; - } - - getSmartSelectionRange() { - return notImplemented(); - } - - findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean): RenameLocation[] { - if (!this.lastRenameEntry || - this.lastRenameEntry.inputs.fileName !== fileName || - this.lastRenameEntry.inputs.position !== position || - this.lastRenameEntry.inputs.findInStrings !== findInStrings || - this.lastRenameEntry.inputs.findInComments !== findInComments) { - this.getRenameInfo(fileName, position, { allowRenameOfImportPath: true }, findInStrings, findInComments); - } - - return this.lastRenameEntry!.locations; - } - - private decodeNavigationBarItems(items: protocol.NavigationBarItem[] | undefined, fileName: string, lineMap: number[]): NavigationBarItem[] { - if (!items) { - return []; - } - - return items.map(item => ({ - text: item.text, - kind: item.kind, - kindModifiers: item.kindModifiers || "", - spans: item.spans.map(span => this.decodeSpan(span, fileName, lineMap)), - childItems: this.decodeNavigationBarItems(item.childItems, fileName, lineMap), - indent: item.indent, - bolded: false, - grayed: false - })); - } - - getNavigationBarItems(file: string): NavigationBarItem[] { - const request = this.processRequest(CommandNames.NavBar, { file }); - const response = this.processResponse(request); - - const lineMap = this.getLineMap(file); - return this.decodeNavigationBarItems(response.body, file, lineMap); - } - - private decodeNavigationTree(tree: protocol.NavigationTree, fileName: string, lineMap: number[]): NavigationTree { - return { - text: tree.text, - kind: tree.kind, - kindModifiers: tree.kindModifiers, - spans: tree.spans.map(span => this.decodeSpan(span, fileName, lineMap)), - nameSpan: tree.nameSpan && this.decodeSpan(tree.nameSpan, fileName, lineMap), - childItems: map(tree.childItems, item => this.decodeNavigationTree(item, fileName, lineMap)) - }; - } - - getNavigationTree(file: string): NavigationTree { - const request = this.processRequest(CommandNames.NavTree, { file }); - const response = this.processResponse(request); - - const lineMap = this.getLineMap(file); - return this.decodeNavigationTree(response.body!, file, lineMap); // TODO: GH#18217 - } - - private decodeSpan(span: protocol.TextSpan & { file: string }): TextSpan; - private decodeSpan(span: protocol.TextSpan, fileName: string, lineMap?: number[]): TextSpan; - private decodeSpan(span: protocol.TextSpan & { file: string }, fileName?: string, lineMap?: number[]): TextSpan { - fileName = fileName || span.file; - lineMap = lineMap || this.getLineMap(fileName); - return createTextSpanFromBounds( - this.lineOffsetToPosition(fileName, span.start, lineMap), - this.lineOffsetToPosition(fileName, span.end, lineMap)); - } - - getNameOrDottedNameSpan(_fileName: string, _startPos: number, _endPos: number): TextSpan { - return notImplemented(); - } - - getBreakpointStatementAtPosition(_fileName: string, _position: number): TextSpan { - return notImplemented(); - } - - getSignatureHelpItems(fileName: string, position: number): SignatureHelpItems | undefined { - const args: protocol.SignatureHelpRequestArgs = this.createFileLocationRequestArgs(fileName, position); - - const request = this.processRequest(CommandNames.SignatureHelp, args); - const response = this.processResponse(request); - - if (!response.body) { - return undefined; - } - - const { items, applicableSpan: encodedApplicableSpan, selectedItemIndex, argumentIndex, argumentCount } = response.body; - - const applicableSpan = this.decodeSpan(encodedApplicableSpan, fileName); - - return { items, applicableSpan, selectedItemIndex, argumentIndex, argumentCount }; - } - - getOccurrencesAtPosition(fileName: string, position: number): ReferenceEntry[] { - const args = this.createFileLocationRequestArgs(fileName, position); - - const request = this.processRequest(CommandNames.Occurrences, args); - const response = this.processResponse(request); - - return response.body!.map(entry => ({ // TODO: GH#18217 - fileName: entry.file, - textSpan: this.decodeSpan(entry), - isWriteAccess: entry.isWriteAccess, - isDefinition: false - })); - } - - getDocumentHighlights(fileName: string, position: number, filesToSearch: string[]): DocumentHighlights[] { - const args: protocol.DocumentHighlightsRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), filesToSearch }; - - const request = this.processRequest(CommandNames.DocumentHighlights, args); - const response = this.processResponse(request); - - return response.body!.map(item => ({ // TODO: GH#18217 - fileName: item.file, - highlightSpans: item.highlightSpans.map(span => ({ - textSpan: this.decodeSpan(span, item.file), - kind: span.kind - })), - })); - } - - getOutliningSpans(file: string): OutliningSpan[] { - const request = this.processRequest(CommandNames.GetOutliningSpans, { file }); - const response = this.processResponse(request); - - return response.body!.map(item => ({ - textSpan: this.decodeSpan(item.textSpan, file), - hintSpan: this.decodeSpan(item.hintSpan, file), - bannerText: item.bannerText, - autoCollapse: item.autoCollapse, - kind: item.kind - })); - } - - getTodoComments(_fileName: string, _descriptors: TodoCommentDescriptor[]): TodoComment[] { - return notImplemented(); - } - - getDocCommentTemplateAtPosition(_fileName: string, _position: number): TextInsertion { - return notImplemented(); - } - - isValidBraceCompletionAtPosition(_fileName: string, _position: number, _openingBrace: number): boolean { - return notImplemented(); - } - - getJsxClosingTagAtPosition(_fileName: string, _position: number): never { - return notImplemented(); - } - - getSpanOfEnclosingComment(_fileName: string, _position: number, _onlyMultiLine: boolean): TextSpan { - return notImplemented(); - } - - getCodeFixesAtPosition(file: string, start: number, end: number, errorCodes: readonly number[]): readonly CodeFixAction[] { - const args: protocol.CodeFixRequestArgs = { ...this.createFileRangeRequestArgs(file, start, end), errorCodes }; - - const request = this.processRequest(CommandNames.GetCodeFixes, args); - const response = this.processResponse(request); - - return response.body!.map(({ fixName, description, changes, commands, fixId, fixAllDescription }) => // TODO: GH#18217 - ({ fixName, description, changes: this.convertChanges(changes, file), commands: commands as CodeActionCommand[], fixId, fixAllDescription })); - } - - getCombinedCodeFix = notImplemented; - - applyCodeActionCommand = notImplemented; - - private createFileLocationOrRangeRequestArgs(positionOrRange: number | TextRange, fileName: string): protocol.FileLocationOrRangeRequestArgs { - return typeof positionOrRange === "number" - ? this.createFileLocationRequestArgs(fileName, positionOrRange) - : this.createFileRangeRequestArgs(fileName, positionOrRange.pos, positionOrRange.end); - } - - private createFileLocationRequestArgs(file: string, position: number): protocol.FileLocationRequestArgs { - const { line, offset } = this.positionToOneBasedLineOffset(file, position); - return { file, line, offset }; - } - - private createFileRangeRequestArgs(file: string, start: number, end: number): protocol.FileRangeRequestArgs { - const { line: startLine, offset: startOffset } = this.positionToOneBasedLineOffset(file, start); - const { line: endLine, offset: endOffset } = this.positionToOneBasedLineOffset(file, end); - return { file, startLine, startOffset, endLine, endOffset }; - } - - private createFileLocationRequestArgsWithEndLineAndOffset(file: string, start: number, end: number): protocol.FileLocationRequestArgs & { endLine: number, endOffset: number } { - const { line, offset } = this.positionToOneBasedLineOffset(file, start); - const { line: endLine, offset: endOffset } = this.positionToOneBasedLineOffset(file, end); - return { file, line, offset, endLine, endOffset }; - } - - getApplicableRefactors(fileName: string, positionOrRange: number | TextRange): ApplicableRefactorInfo[] { - const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName); - - const request = this.processRequest(CommandNames.GetApplicableRefactors, args); - const response = this.processResponse(request); - return response.body!; // TODO: GH#18217 - } - - getEditsForRefactor( - fileName: string, - _formatOptions: FormatCodeSettings, - positionOrRange: number | TextRange, - refactorName: string, - actionName: string): RefactorEditInfo { - - const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName) as protocol.GetEditsForRefactorRequestArgs; - args.refactor = refactorName; - args.action = actionName; - - const request = this.processRequest(CommandNames.GetEditsForRefactor, args); - const response = this.processResponse(request); - - if (!response.body) { - return { edits: [], renameFilename: undefined, renameLocation: undefined }; - } - - const edits: FileTextChanges[] = this.convertCodeEditsToTextChanges(response.body.edits); - - const renameFilename: string | undefined = response.body.renameFilename; - let renameLocation: number | undefined; - if (renameFilename !== undefined) { - renameLocation = this.lineOffsetToPosition(renameFilename, response.body.renameLocation!); // TODO: GH#18217 - } - - return { - edits, - renameFilename, - renameLocation - }; - } - - organizeImports(_scope: OrganizeImportsScope, _formatOptions: FormatCodeSettings): readonly FileTextChanges[] { - return notImplemented(); - } - - getEditsForFileRename() { - return notImplemented(); - } - - private convertCodeEditsToTextChanges(edits: protocol.FileCodeEdits[]): FileTextChanges[] { - return edits.map(edit => { - const fileName = edit.fileName; - return { - fileName, - textChanges: edit.textChanges.map(t => this.convertTextChangeToCodeEdit(t, fileName)) - }; - }); - } - - private convertChanges(changes: protocol.FileCodeEdits[], fileName: string): FileTextChanges[] { - return changes.map(change => ({ - fileName: change.fileName, - textChanges: change.textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, fileName)) - })); - } - - convertTextChangeToCodeEdit(change: protocol.CodeEdit, fileName: string): TextChange { - return { - span: this.decodeSpan(change, fileName), - newText: change.newText ? change.newText : "" - }; - } - - getBraceMatchingAtPosition(fileName: string, position: number): TextSpan[] { - const args = this.createFileLocationRequestArgs(fileName, position); - - const request = this.processRequest(CommandNames.Brace, args); - const response = this.processResponse(request); - - return response.body!.map(entry => this.decodeSpan(entry, fileName)); // TODO: GH#18217 - } - - configurePlugin(pluginName: string, configuration: any): void { - const request = this.processRequest("configurePlugin", { pluginName, configuration }); - this.processResponse(request, /*expectEmptyBody*/ true); - } - - getIndentationAtPosition(_fileName: string, _position: number, _options: EditorOptions): number { - return notImplemented(); - } - - getSyntacticClassifications(_fileName: string, _span: TextSpan): ClassifiedSpan[] { - return notImplemented(); - } - - getSemanticClassifications(_fileName: string, _span: TextSpan): ClassifiedSpan[] { - return notImplemented(); - } - - getEncodedSyntacticClassifications(_fileName: string, _span: TextSpan): Classifications { - return notImplemented(); - } - - getEncodedSemanticClassifications(_fileName: string, _span: TextSpan): Classifications { - return notImplemented(); - } - - private convertCallHierarchyItem(item: protocol.CallHierarchyItem): CallHierarchyItem { - return { - file: item.file, - name: item.name, - kind: item.kind, - span: this.decodeSpan(item.span, item.file), - selectionSpan: this.decodeSpan(item.selectionSpan, item.file) - }; - } - - prepareCallHierarchy(fileName: string, position: number): CallHierarchyItem | CallHierarchyItem[] | undefined { - const args = this.createFileLocationRequestArgs(fileName, position); - const request = this.processRequest(CommandNames.PrepareCallHierarchy, args); - const response = this.processResponse(request); - return response.body && mapOneOrMany(response.body, item => this.convertCallHierarchyItem(item)); - } - - private convertCallHierarchyIncomingCall(item: protocol.CallHierarchyIncomingCall): CallHierarchyIncomingCall { - return { - from: this.convertCallHierarchyItem(item.from), - fromSpans: item.fromSpans.map(span => this.decodeSpan(span, item.from.file)) - }; - } - - provideCallHierarchyIncomingCalls(fileName: string, position: number) { - const args = this.createFileLocationRequestArgs(fileName, position); - const request = this.processRequest(CommandNames.PrepareCallHierarchy, args); - const response = this.processResponse(request); - return response.body.map(item => this.convertCallHierarchyIncomingCall(item)); - } - - private convertCallHierarchyOutgoingCall(file: string, item: protocol.CallHierarchyOutgoingCall): CallHierarchyOutgoingCall { - return { - to: this.convertCallHierarchyItem(item.to), - fromSpans: item.fromSpans.map(span => this.decodeSpan(span, file)) - }; - } - - provideCallHierarchyOutgoingCalls(fileName: string, position: number) { - const args = this.createFileLocationRequestArgs(fileName, position); - const request = this.processRequest(CommandNames.PrepareCallHierarchy, args); - const response = this.processResponse(request); - return response.body.map(item => this.convertCallHierarchyOutgoingCall(fileName, item)); - } - - getProgram(): Program { - throw new Error("SourceFile objects are not serializable through the server protocol."); - } - - getNonBoundSourceFile(_fileName: string): SourceFile { - throw new Error("SourceFile objects are not serializable through the server protocol."); - } - - getSourceFile(_fileName: string): SourceFile { - throw new Error("SourceFile objects are not serializable through the server protocol."); - } - - cleanupSemanticCache(): void { - throw new Error("cleanupSemanticCache is not available through the server layer."); - } - - getSourceMapper(): never { - return notImplemented(); - } - - clearSourceMapperCache(): never { - return notImplemented(); - } - - toggleLineComment(): TextChange[] { - return notImplemented(); - } - - toggleMultilineComment(): TextChange[] { - return notImplemented(); - } - - commentSelection(): TextChange[] { - return notImplemented(); - } - - uncommentSelection(): TextChange[] { - return notImplemented(); - } - - dispose(): void { - throw new Error("dispose is not available through the server layer."); - } - } -} +namespace ts.server { + export interface SessionClientHost extends LanguageServiceHost { + writeMessage(message: string): void; + } + + interface RenameEntry { + readonly renameInfo: RenameInfo; + readonly inputs: { + readonly fileName: string; + readonly position: number; + readonly findInStrings: boolean; + readonly findInComments: boolean; + }; + readonly locations: RenameLocation[]; + } + + /* @internal */ + export function extractMessage(message: string): string { + // Read the content length + const contentLengthPrefix = "Content-Length: "; + const lines = message.split(/\r?\n/); + Debug.assert(lines.length >= 2, "Malformed response: Expected 3 lines in the response."); + + const contentLengthText = lines[0]; + Debug.assert(contentLengthText.indexOf(contentLengthPrefix) === 0, "Malformed response: Response text did not contain content-length header."); + const contentLength = parseInt(contentLengthText.substring(contentLengthPrefix.length)); + + // Read the body + const responseBody = lines[2]; + + // Verify content length + Debug.assert(responseBody.length + 1 === contentLength, "Malformed response: Content length did not match the response's body length."); + return responseBody; + } + + export class SessionClient implements LanguageService { + private sequence = 0; + private lineMaps: Map = createMap(); + private messages: string[] = []; + private lastRenameEntry: RenameEntry | undefined; + + constructor(private host: SessionClientHost) { + } + + public onMessage(message: string): void { + this.messages.push(message); + } + + private writeMessage(message: string): void { + this.host.writeMessage(message); + } + + private getLineMap(fileName: string): number[] { + let lineMap = this.lineMaps.get(fileName); + if (!lineMap) { + lineMap = computeLineStarts(getSnapshotText(this.host.getScriptSnapshot(fileName)!)); + this.lineMaps.set(fileName, lineMap); + } + return lineMap; + } + + private lineOffsetToPosition(fileName: string, lineOffset: protocol.Location, lineMap?: number[]): number { + lineMap = lineMap || this.getLineMap(fileName); + return computePositionOfLineAndCharacter(lineMap, lineOffset.line - 1, lineOffset.offset - 1); + } + + private positionToOneBasedLineOffset(fileName: string, position: number): protocol.Location { + const lineOffset = computeLineAndCharacterOfPosition(this.getLineMap(fileName), position); + return { + line: lineOffset.line + 1, + offset: lineOffset.character + 1 + }; + } + + private convertCodeEditsToTextChange(fileName: string, codeEdit: protocol.CodeEdit): TextChange { + return { span: this.decodeSpan(codeEdit, fileName), newText: codeEdit.newText }; + } + + private processRequest(command: string, args: T["arguments"]): T { + const request: protocol.Request = { + seq: this.sequence, + type: "request", + arguments: args, + command + }; + this.sequence++; + + this.writeMessage(JSON.stringify(request)); + + return request; + } + + private processResponse(request: protocol.Request, expectEmptyBody = false): T { + let foundResponseMessage = false; + let response!: T; + while (!foundResponseMessage) { + const lastMessage = this.messages.shift()!; + Debug.assert(!!lastMessage, "Did not receive any responses."); + const responseBody = extractMessage(lastMessage); + try { + response = JSON.parse(responseBody); + // the server may emit events before emitting the response. We + // want to ignore these events for testing purpose. + if (response.type === "response") { + foundResponseMessage = true; + } + } + catch (e) { + throw new Error("Malformed response: Failed to parse server response: " + lastMessage + ". \r\n Error details: " + e.message); + } + } + + // verify the sequence numbers + Debug.assert(response.request_seq === request.seq, "Malformed response: response sequence number did not match request sequence number."); + + // unmarshal errors + if (!response.success) { + throw new Error("Error " + response.message); + } + + Debug.assert(expectEmptyBody || !!response.body, "Malformed response: Unexpected empty response body."); + Debug.assert(!expectEmptyBody || !response.body, "Malformed response: Unexpected non-empty response body."); + + return response; + } + + /*@internal*/ + configure(preferences: UserPreferences) { + const args: protocol.ConfigureRequestArguments = { preferences }; + const request = this.processRequest(CommandNames.Configure, args); + this.processResponse(request, /*expectEmptyBody*/ true); + } + + openFile(file: string, fileContent?: string, scriptKindName?: "TS" | "JS" | "TSX" | "JSX"): void { + const args: protocol.OpenRequestArgs = { file, fileContent, scriptKindName }; + this.processRequest(CommandNames.Open, args); + } + + closeFile(file: string): void { + const args: protocol.FileRequestArgs = { file }; + this.processRequest(CommandNames.Close, args); + } + + createChangeFileRequestArgs(fileName: string, start: number, end: number, insertString: string): protocol.ChangeRequestArgs { + return { ...this.createFileLocationRequestArgsWithEndLineAndOffset(fileName, start, end), insertString }; + } + + changeFile(fileName: string, args: protocol.ChangeRequestArgs): void { + // clear the line map after an edit + this.lineMaps.set(fileName, undefined!); // TODO: GH#18217 + this.processRequest(CommandNames.Change, args); + } + + toLineColumnOffset(fileName: string, position: number) { + const { line, offset } = this.positionToOneBasedLineOffset(fileName, position); + return { line, character: offset }; + } + + getQuickInfoAtPosition(fileName: string, position: number): QuickInfo { + const args = this.createFileLocationRequestArgs(fileName, position); + + const request = this.processRequest(CommandNames.Quickinfo, args); + const response = this.processResponse(request); + const body = response.body!; // TODO: GH#18217 + + return { + kind: body.kind, + kindModifiers: body.kindModifiers, + textSpan: this.decodeSpan(body, fileName), + displayParts: [{ kind: "text", text: body.displayString }], + documentation: [{ kind: "text", text: body.documentation }], + tags: body.tags + }; + } + + getProjectInfo(file: string, needFileNameList: boolean): protocol.ProjectInfo { + const args: protocol.ProjectInfoRequestArgs = { file, needFileNameList }; + + const request = this.processRequest(CommandNames.ProjectInfo, args); + const response = this.processResponse(request); + + return { + configFileName: response.body!.configFileName, // TODO: GH#18217 + fileNames: response.body!.fileNames + }; + } + + getCompletionsAtPosition(fileName: string, position: number, _preferences: UserPreferences | undefined): CompletionInfo { + // Not passing along 'preferences' because server should already have those from the 'configure' command + const args: protocol.CompletionsRequestArgs = this.createFileLocationRequestArgs(fileName, position); + + const request = this.processRequest(CommandNames.Completions, args); + const response = this.processResponse(request); + + return { + isGlobalCompletion: false, + isMemberCompletion: false, + isNewIdentifierLocation: false, + entries: response.body!.map(entry => { // TODO: GH#18217 + if (entry.replacementSpan !== undefined) { + const { name, kind, kindModifiers, sortText, replacementSpan, hasAction, source, isRecommended } = entry; + // TODO: GH#241 + const res: CompletionEntry = { name, kind, kindModifiers, sortText, replacementSpan: this.decodeSpan(replacementSpan, fileName), hasAction, source, isRecommended }; + return res; + } + + return entry as { name: string, kind: ScriptElementKind, kindModifiers: string, sortText: string }; // TODO: GH#18217 + }) + }; + } + + getCompletionEntryDetails(fileName: string, position: number, entryName: string, _options: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined): CompletionEntryDetails { + const args: protocol.CompletionDetailsRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), entryNames: [{ name: entryName, source }] }; + + const request = this.processRequest(CommandNames.CompletionDetails, args); + const response = this.processResponse(request); + Debug.assert(response.body!.length === 1, "Unexpected length of completion details response body."); + const convertedCodeActions = map(response.body![0].codeActions, ({ description, changes }) => ({ description, changes: this.convertChanges(changes, fileName) })); + return { ...response.body![0], codeActions: convertedCodeActions }; + } + + getCompletionEntrySymbol(_fileName: string, _position: number, _entryName: string): Symbol { + return notImplemented(); + } + + getNavigateToItems(searchValue: string): NavigateToItem[] { + const args: protocol.NavtoRequestArgs = { + searchValue, + file: this.host.getScriptFileNames()[0] + }; + + const request = this.processRequest(CommandNames.Navto, args); + const response = this.processResponse(request); + + return response.body!.map(entry => ({ // TODO: GH#18217 + name: entry.name, + containerName: entry.containerName || "", + containerKind: entry.containerKind || ScriptElementKind.unknown, + kind: entry.kind, + kindModifiers: entry.kindModifiers || "", + matchKind: entry.matchKind as keyof typeof PatternMatchKind, + isCaseSensitive: entry.isCaseSensitive, + fileName: entry.file, + textSpan: this.decodeSpan(entry), + })); + } + + getFormattingEditsForRange(file: string, start: number, end: number, _options: FormatCodeOptions): TextChange[] { + const args: protocol.FormatRequestArgs = this.createFileLocationRequestArgsWithEndLineAndOffset(file, start, end); + + + // TODO: handle FormatCodeOptions + const request = this.processRequest(CommandNames.Format, args); + const response = this.processResponse(request); + + return response.body!.map(entry => this.convertCodeEditsToTextChange(file, entry)); // TODO: GH#18217 + } + + getFormattingEditsForDocument(fileName: string, options: FormatCodeOptions): TextChange[] { + return this.getFormattingEditsForRange(fileName, 0, this.host.getScriptSnapshot(fileName)!.getLength(), options); + } + + getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, _options: FormatCodeOptions): TextChange[] { + const args: protocol.FormatOnKeyRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), key }; + + // TODO: handle FormatCodeOptions + const request = this.processRequest(CommandNames.Formatonkey, args); + const response = this.processResponse(request); + + return response.body!.map(entry => this.convertCodeEditsToTextChange(fileName, entry)); // TODO: GH#18217 + } + + getDefinitionAtPosition(fileName: string, position: number): DefinitionInfo[] { + const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position); + + const request = this.processRequest(CommandNames.Definition, args); + const response = this.processResponse(request); + + return response.body!.map(entry => ({ // TODO: GH#18217 + containerKind: ScriptElementKind.unknown, + containerName: "", + fileName: entry.file, + textSpan: this.decodeSpan(entry), + kind: ScriptElementKind.unknown, + name: "" + })); + } + + getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan { + const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position); + + const request = this.processRequest(CommandNames.DefinitionAndBoundSpan, args); + const response = this.processResponse(request); + const body = Debug.checkDefined(response.body); // TODO: GH#18217 + + return { + definitions: body.definitions.map(entry => ({ + containerKind: ScriptElementKind.unknown, + containerName: "", + fileName: entry.file, + textSpan: this.decodeSpan(entry), + kind: ScriptElementKind.unknown, + name: "" + })), + textSpan: this.decodeSpan(body.textSpan, request.arguments.file) + }; + } + + getTypeDefinitionAtPosition(fileName: string, position: number): DefinitionInfo[] { + const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position); + + const request = this.processRequest(CommandNames.TypeDefinition, args); + const response = this.processResponse(request); + + return response.body!.map(entry => ({ // TODO: GH#18217 + containerKind: ScriptElementKind.unknown, + containerName: "", + fileName: entry.file, + textSpan: this.decodeSpan(entry), + kind: ScriptElementKind.unknown, + name: "" + })); + } + + getImplementationAtPosition(fileName: string, position: number): ImplementationLocation[] { + const args = this.createFileLocationRequestArgs(fileName, position); + + const request = this.processRequest(CommandNames.Implementation, args); + const response = this.processResponse(request); + + return response.body!.map(entry => ({ // TODO: GH#18217 + fileName: entry.file, + textSpan: this.decodeSpan(entry), + kind: ScriptElementKind.unknown, + displayParts: [] + })); + } + + findReferences(_fileName: string, _position: number): ReferencedSymbol[] { + // Not yet implemented. + return []; + } + + getReferencesAtPosition(fileName: string, position: number): ReferenceEntry[] { + const args = this.createFileLocationRequestArgs(fileName, position); + + const request = this.processRequest(CommandNames.References, args); + const response = this.processResponse(request); + + return response.body!.refs.map(entry => ({ // TODO: GH#18217 + fileName: entry.file, + textSpan: this.decodeSpan(entry), + isWriteAccess: entry.isWriteAccess, + isDefinition: entry.isDefinition, + })); + } + + getEmitOutput(file: string): EmitOutput { + const request = this.processRequest(protocol.CommandTypes.EmitOutput, { file }); + const response = this.processResponse(request); + return response.body as EmitOutput; + } + + getSyntacticDiagnostics(file: string): DiagnosticWithLocation[] { + return this.getDiagnostics(file, CommandNames.SyntacticDiagnosticsSync); + } + getSemanticDiagnostics(file: string): Diagnostic[] { + return this.getDiagnostics(file, CommandNames.SemanticDiagnosticsSync); + } + getSuggestionDiagnostics(file: string): DiagnosticWithLocation[] { + return this.getDiagnostics(file, CommandNames.SuggestionDiagnosticsSync); + } + + private getDiagnostics(file: string, command: CommandNames): DiagnosticWithLocation[] { + const request = this.processRequest(command, { file, includeLinePosition: true }); + const response = this.processResponse(request); + const sourceText = getSnapshotText(this.host.getScriptSnapshot(file)!); + const fakeSourceFile = { fileName: file, text: sourceText } as SourceFile; // Warning! This is a huge lie! + + return (response.body).map((entry): DiagnosticWithLocation => { + const category = firstDefined(Object.keys(DiagnosticCategory), id => + isString(id) && entry.category === id.toLowerCase() ? (DiagnosticCategory)[id] : undefined); + return { + file: fakeSourceFile, + start: entry.start, + length: entry.length, + messageText: entry.message, + category: Debug.checkDefined(category, "convertDiagnostic: category should not be undefined"), + code: entry.code, + reportsUnnecessary: entry.reportsUnnecessary, + }; + }); + } + + getCompilerOptionsDiagnostics(): Diagnostic[] { + return notImplemented(); + } + + getRenameInfo(fileName: string, position: number, _options?: RenameInfoOptions, findInStrings?: boolean, findInComments?: boolean): RenameInfo { + // Not passing along 'options' because server should already have those from the 'configure' command + const args: protocol.RenameRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), findInStrings, findInComments }; + + const request = this.processRequest(CommandNames.Rename, args); + const response = this.processResponse(request); + const body = response.body!; // TODO: GH#18217 + const locations: RenameLocation[] = []; + for (const entry of body.locs) { + const fileName = entry.file; + for (const { start, end, contextStart, contextEnd, ...prefixSuffixText } of entry.locs) { + locations.push({ + textSpan: this.decodeSpan({ start, end }, fileName), + fileName, + ...(contextStart !== undefined ? + { contextSpan: this.decodeSpan({ start: contextStart, end: contextEnd! }, fileName) } : + undefined), + ...prefixSuffixText + }); + } + } + + const renameInfo = body.info.canRename + ? identity({ + canRename: body.info.canRename, + fileToRename: body.info.fileToRename, + displayName: body.info.displayName, + fullDisplayName: body.info.fullDisplayName, + kind: body.info.kind, + kindModifiers: body.info.kindModifiers, + triggerSpan: createTextSpanFromBounds(position, position), + }) + : identity({ canRename: false, localizedErrorMessage: body.info.localizedErrorMessage }); + this.lastRenameEntry = { + renameInfo, + inputs: { + fileName, + position, + findInStrings: !!findInStrings, + findInComments: !!findInComments, + }, + locations, + }; + return renameInfo; + } + + getSmartSelectionRange() { + return notImplemented(); + } + + findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean): RenameLocation[] { + if (!this.lastRenameEntry || + this.lastRenameEntry.inputs.fileName !== fileName || + this.lastRenameEntry.inputs.position !== position || + this.lastRenameEntry.inputs.findInStrings !== findInStrings || + this.lastRenameEntry.inputs.findInComments !== findInComments) { + this.getRenameInfo(fileName, position, { allowRenameOfImportPath: true }, findInStrings, findInComments); + } + + return this.lastRenameEntry!.locations; + } + + private decodeNavigationBarItems(items: protocol.NavigationBarItem[] | undefined, fileName: string, lineMap: number[]): NavigationBarItem[] { + if (!items) { + return []; + } + + return items.map(item => ({ + text: item.text, + kind: item.kind, + kindModifiers: item.kindModifiers || "", + spans: item.spans.map(span => this.decodeSpan(span, fileName, lineMap)), + childItems: this.decodeNavigationBarItems(item.childItems, fileName, lineMap), + indent: item.indent, + bolded: false, + grayed: false + })); + } + + getNavigationBarItems(file: string): NavigationBarItem[] { + const request = this.processRequest(CommandNames.NavBar, { file }); + const response = this.processResponse(request); + + const lineMap = this.getLineMap(file); + return this.decodeNavigationBarItems(response.body, file, lineMap); + } + + private decodeNavigationTree(tree: protocol.NavigationTree, fileName: string, lineMap: number[]): NavigationTree { + return { + text: tree.text, + kind: tree.kind, + kindModifiers: tree.kindModifiers, + spans: tree.spans.map(span => this.decodeSpan(span, fileName, lineMap)), + nameSpan: tree.nameSpan && this.decodeSpan(tree.nameSpan, fileName, lineMap), + childItems: map(tree.childItems, item => this.decodeNavigationTree(item, fileName, lineMap)) + }; + } + + getNavigationTree(file: string): NavigationTree { + const request = this.processRequest(CommandNames.NavTree, { file }); + const response = this.processResponse(request); + + const lineMap = this.getLineMap(file); + return this.decodeNavigationTree(response.body!, file, lineMap); // TODO: GH#18217 + } + + private decodeSpan(span: protocol.TextSpan & { file: string }): TextSpan; + private decodeSpan(span: protocol.TextSpan, fileName: string, lineMap?: number[]): TextSpan; + private decodeSpan(span: protocol.TextSpan & { file: string }, fileName?: string, lineMap?: number[]): TextSpan { + fileName = fileName || span.file; + lineMap = lineMap || this.getLineMap(fileName); + return createTextSpanFromBounds( + this.lineOffsetToPosition(fileName, span.start, lineMap), + this.lineOffsetToPosition(fileName, span.end, lineMap)); + } + + getNameOrDottedNameSpan(_fileName: string, _startPos: number, _endPos: number): TextSpan { + return notImplemented(); + } + + getBreakpointStatementAtPosition(_fileName: string, _position: number): TextSpan { + return notImplemented(); + } + + getSignatureHelpItems(fileName: string, position: number): SignatureHelpItems | undefined { + const args: protocol.SignatureHelpRequestArgs = this.createFileLocationRequestArgs(fileName, position); + + const request = this.processRequest(CommandNames.SignatureHelp, args); + const response = this.processResponse(request); + + if (!response.body) { + return undefined; + } + + const { items, applicableSpan: encodedApplicableSpan, selectedItemIndex, argumentIndex, argumentCount } = response.body; + + const applicableSpan = this.decodeSpan(encodedApplicableSpan, fileName); + + return { items, applicableSpan, selectedItemIndex, argumentIndex, argumentCount }; + } + + getOccurrencesAtPosition(fileName: string, position: number): ReferenceEntry[] { + const args = this.createFileLocationRequestArgs(fileName, position); + + const request = this.processRequest(CommandNames.Occurrences, args); + const response = this.processResponse(request); + + return response.body!.map(entry => ({ // TODO: GH#18217 + fileName: entry.file, + textSpan: this.decodeSpan(entry), + isWriteAccess: entry.isWriteAccess, + isDefinition: false + })); + } + + getDocumentHighlights(fileName: string, position: number, filesToSearch: string[]): DocumentHighlights[] { + const args: protocol.DocumentHighlightsRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), filesToSearch }; + + const request = this.processRequest(CommandNames.DocumentHighlights, args); + const response = this.processResponse(request); + + return response.body!.map(item => ({ // TODO: GH#18217 + fileName: item.file, + highlightSpans: item.highlightSpans.map(span => ({ + textSpan: this.decodeSpan(span, item.file), + kind: span.kind + })), + })); + } + + getOutliningSpans(file: string): OutliningSpan[] { + const request = this.processRequest(CommandNames.GetOutliningSpans, { file }); + const response = this.processResponse(request); + + return response.body!.map(item => ({ + textSpan: this.decodeSpan(item.textSpan, file), + hintSpan: this.decodeSpan(item.hintSpan, file), + bannerText: item.bannerText, + autoCollapse: item.autoCollapse, + kind: item.kind + })); + } + + getTodoComments(_fileName: string, _descriptors: TodoCommentDescriptor[]): TodoComment[] { + return notImplemented(); + } + + getDocCommentTemplateAtPosition(_fileName: string, _position: number): TextInsertion { + return notImplemented(); + } + + isValidBraceCompletionAtPosition(_fileName: string, _position: number, _openingBrace: number): boolean { + return notImplemented(); + } + + getJsxClosingTagAtPosition(_fileName: string, _position: number): never { + return notImplemented(); + } + + getSpanOfEnclosingComment(_fileName: string, _position: number, _onlyMultiLine: boolean): TextSpan { + return notImplemented(); + } + + getCodeFixesAtPosition(file: string, start: number, end: number, errorCodes: readonly number[]): readonly CodeFixAction[] { + const args: protocol.CodeFixRequestArgs = { ...this.createFileRangeRequestArgs(file, start, end), errorCodes }; + + const request = this.processRequest(CommandNames.GetCodeFixes, args); + const response = this.processResponse(request); + + return response.body!.map(({ fixName, description, changes, commands, fixId, fixAllDescription }) => // TODO: GH#18217 + ({ fixName, description, changes: this.convertChanges(changes, file), commands: commands as CodeActionCommand[], fixId, fixAllDescription })); + } + + getCombinedCodeFix = notImplemented; + + applyCodeActionCommand = notImplemented; + + private createFileLocationOrRangeRequestArgs(positionOrRange: number | TextRange, fileName: string): protocol.FileLocationOrRangeRequestArgs { + return typeof positionOrRange === "number" + ? this.createFileLocationRequestArgs(fileName, positionOrRange) + : this.createFileRangeRequestArgs(fileName, positionOrRange.pos, positionOrRange.end); + } + + private createFileLocationRequestArgs(file: string, position: number): protocol.FileLocationRequestArgs { + const { line, offset } = this.positionToOneBasedLineOffset(file, position); + return { file, line, offset }; + } + + private createFileRangeRequestArgs(file: string, start: number, end: number): protocol.FileRangeRequestArgs { + const { line: startLine, offset: startOffset } = this.positionToOneBasedLineOffset(file, start); + const { line: endLine, offset: endOffset } = this.positionToOneBasedLineOffset(file, end); + return { file, startLine, startOffset, endLine, endOffset }; + } + + private createFileLocationRequestArgsWithEndLineAndOffset(file: string, start: number, end: number): protocol.FileLocationRequestArgs & { endLine: number, endOffset: number } { + const { line, offset } = this.positionToOneBasedLineOffset(file, start); + const { line: endLine, offset: endOffset } = this.positionToOneBasedLineOffset(file, end); + return { file, line, offset, endLine, endOffset }; + } + + getApplicableRefactors(fileName: string, positionOrRange: number | TextRange): ApplicableRefactorInfo[] { + const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName); + + const request = this.processRequest(CommandNames.GetApplicableRefactors, args); + const response = this.processResponse(request); + return response.body!; // TODO: GH#18217 + } + + getEditsForRefactor( + fileName: string, + _formatOptions: FormatCodeSettings, + positionOrRange: number | TextRange, + refactorName: string, + actionName: string): RefactorEditInfo { + + const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName) as protocol.GetEditsForRefactorRequestArgs; + args.refactor = refactorName; + args.action = actionName; + + const request = this.processRequest(CommandNames.GetEditsForRefactor, args); + const response = this.processResponse(request); + + if (!response.body) { + return { edits: [], renameFilename: undefined, renameLocation: undefined }; + } + + const edits: FileTextChanges[] = this.convertCodeEditsToTextChanges(response.body.edits); + + const renameFilename: string | undefined = response.body.renameFilename; + let renameLocation: number | undefined; + if (renameFilename !== undefined) { + renameLocation = this.lineOffsetToPosition(renameFilename, response.body.renameLocation!); // TODO: GH#18217 + } + + return { + edits, + renameFilename, + renameLocation + }; + } + + organizeImports(_scope: OrganizeImportsScope, _formatOptions: FormatCodeSettings): readonly FileTextChanges[] { + return notImplemented(); + } + + getEditsForFileRename() { + return notImplemented(); + } + + private convertCodeEditsToTextChanges(edits: protocol.FileCodeEdits[]): FileTextChanges[] { + return edits.map(edit => { + const fileName = edit.fileName; + return { + fileName, + textChanges: edit.textChanges.map(t => this.convertTextChangeToCodeEdit(t, fileName)) + }; + }); + } + + private convertChanges(changes: protocol.FileCodeEdits[], fileName: string): FileTextChanges[] { + return changes.map(change => ({ + fileName: change.fileName, + textChanges: change.textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, fileName)) + })); + } + + convertTextChangeToCodeEdit(change: protocol.CodeEdit, fileName: string): TextChange { + return { + span: this.decodeSpan(change, fileName), + newText: change.newText ? change.newText : "" + }; + } + + getBraceMatchingAtPosition(fileName: string, position: number): TextSpan[] { + const args = this.createFileLocationRequestArgs(fileName, position); + + const request = this.processRequest(CommandNames.Brace, args); + const response = this.processResponse(request); + + return response.body!.map(entry => this.decodeSpan(entry, fileName)); // TODO: GH#18217 + } + + configurePlugin(pluginName: string, configuration: any): void { + const request = this.processRequest("configurePlugin", { pluginName, configuration }); + this.processResponse(request, /*expectEmptyBody*/ true); + } + + getIndentationAtPosition(_fileName: string, _position: number, _options: EditorOptions): number { + return notImplemented(); + } + + getSyntacticClassifications(_fileName: string, _span: TextSpan): ClassifiedSpan[] { + return notImplemented(); + } + + getSemanticClassifications(_fileName: string, _span: TextSpan): ClassifiedSpan[] { + return notImplemented(); + } + + getEncodedSyntacticClassifications(_fileName: string, _span: TextSpan): Classifications { + return notImplemented(); + } + + getEncodedSemanticClassifications(_fileName: string, _span: TextSpan): Classifications { + return notImplemented(); + } + + private convertCallHierarchyItem(item: protocol.CallHierarchyItem): CallHierarchyItem { + return { + file: item.file, + name: item.name, + kind: item.kind, + span: this.decodeSpan(item.span, item.file), + selectionSpan: this.decodeSpan(item.selectionSpan, item.file) + }; + } + + prepareCallHierarchy(fileName: string, position: number): CallHierarchyItem | CallHierarchyItem[] | undefined { + const args = this.createFileLocationRequestArgs(fileName, position); + const request = this.processRequest(CommandNames.PrepareCallHierarchy, args); + const response = this.processResponse(request); + return response.body && mapOneOrMany(response.body, item => this.convertCallHierarchyItem(item)); + } + + private convertCallHierarchyIncomingCall(item: protocol.CallHierarchyIncomingCall): CallHierarchyIncomingCall { + return { + from: this.convertCallHierarchyItem(item.from), + fromSpans: item.fromSpans.map(span => this.decodeSpan(span, item.from.file)) + }; + } + + provideCallHierarchyIncomingCalls(fileName: string, position: number) { + const args = this.createFileLocationRequestArgs(fileName, position); + const request = this.processRequest(CommandNames.PrepareCallHierarchy, args); + const response = this.processResponse(request); + return response.body.map(item => this.convertCallHierarchyIncomingCall(item)); + } + + private convertCallHierarchyOutgoingCall(file: string, item: protocol.CallHierarchyOutgoingCall): CallHierarchyOutgoingCall { + return { + to: this.convertCallHierarchyItem(item.to), + fromSpans: item.fromSpans.map(span => this.decodeSpan(span, file)) + }; + } + + provideCallHierarchyOutgoingCalls(fileName: string, position: number) { + const args = this.createFileLocationRequestArgs(fileName, position); + const request = this.processRequest(CommandNames.PrepareCallHierarchy, args); + const response = this.processResponse(request); + return response.body.map(item => this.convertCallHierarchyOutgoingCall(fileName, item)); + } + + getProgram(): Program { + throw new Error("SourceFile objects are not serializable through the server protocol."); + } + + getNonBoundSourceFile(_fileName: string): SourceFile { + throw new Error("SourceFile objects are not serializable through the server protocol."); + } + + getSourceFile(_fileName: string): SourceFile { + throw new Error("SourceFile objects are not serializable through the server protocol."); + } + + cleanupSemanticCache(): void { + throw new Error("cleanupSemanticCache is not available through the server layer."); + } + + getSourceMapper(): never { + return notImplemented(); + } + + clearSourceMapperCache(): never { + return notImplemented(); + } + + toggleLineComment(): TextChange[] { + return notImplemented(); + } + + toggleMultilineComment(): TextChange[] { + return notImplemented(); + } + + commentSelection(): TextChange[] { + return notImplemented(); + } + + uncommentSelection(): TextChange[] { + return notImplemented(); + } + + dispose(): void { + throw new Error("dispose is not available through the server layer."); + } + } +} diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 7bc137cb4e..6c6b1e9ea5 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -1,991 +1,991 @@ -namespace Harness.LanguageService { - - export function makeDefaultProxy(info: ts.server.PluginCreateInfo): ts.LanguageService { - const proxy = Object.create(/*prototype*/ null); // eslint-disable-line no-null/no-null - const langSvc: any = info.languageService; - for (const k of Object.keys(langSvc)) { - // eslint-disable-next-line only-arrow-functions - proxy[k] = function () { - return langSvc[k].apply(langSvc, arguments); - }; - } - return proxy; - } - - export class ScriptInfo { - public version = 1; - public editRanges: { length: number; textChangeRange: ts.TextChangeRange; }[] = []; - private lineMap: number[] | undefined; - - constructor(public fileName: string, public content: string, public isRootFile: boolean) { - this.setContent(content); - } - - private setContent(content: string): void { - this.content = content; - this.lineMap = undefined; - } - - public getLineMap(): number[] { - return this.lineMap || (this.lineMap = ts.computeLineStarts(this.content)); - } - - public updateContent(content: string): void { - this.editRanges = []; - this.setContent(content); - this.version++; - } - - public editContent(start: number, end: number, newText: string): void { - // Apply edits - const prefix = this.content.substring(0, start); - const middle = newText; - const suffix = this.content.substring(end); - this.setContent(prefix + middle + suffix); - - // Store edit range + new length of script - this.editRanges.push({ - length: this.content.length, - textChangeRange: ts.createTextChangeRange( - ts.createTextSpanFromBounds(start, end), newText.length) - }); - - // Update version # - this.version++; - } - - public getTextChangeRangeBetweenVersions(startVersion: number, endVersion: number): ts.TextChangeRange { - if (startVersion === endVersion) { - // No edits! - return ts.unchangedTextChangeRange; - } - - const initialEditRangeIndex = this.editRanges.length - (this.version - startVersion); - const lastEditRangeIndex = this.editRanges.length - (this.version - endVersion); - - const entries = this.editRanges.slice(initialEditRangeIndex, lastEditRangeIndex); - return ts.collapseTextChangeRangesAcrossMultipleVersions(entries.map(e => e.textChangeRange)); - } - } - - class ScriptSnapshot implements ts.IScriptSnapshot { - public textSnapshot: string; - public version: number; - - constructor(public scriptInfo: ScriptInfo) { - this.textSnapshot = scriptInfo.content; - this.version = scriptInfo.version; - } - - public getText(start: number, end: number): string { - return this.textSnapshot.substring(start, end); - } - - public getLength(): number { - return this.textSnapshot.length; - } - - public getChangeRange(oldScript: ts.IScriptSnapshot): ts.TextChangeRange { - const oldShim = oldScript; - return this.scriptInfo.getTextChangeRangeBetweenVersions(oldShim.version, this.version); - } - } - - class ScriptSnapshotProxy implements ts.ScriptSnapshotShim { - constructor(private readonly scriptSnapshot: ts.IScriptSnapshot) { - } - - public getText(start: number, end: number): string { - return this.scriptSnapshot.getText(start, end); - } - - public getLength(): number { - return this.scriptSnapshot.getLength(); - } - - public getChangeRange(oldScript: ts.ScriptSnapshotShim): string | undefined { - const range = this.scriptSnapshot.getChangeRange((oldScript as ScriptSnapshotProxy).scriptSnapshot); - return range && JSON.stringify(range); - } - } - - class DefaultHostCancellationToken implements ts.HostCancellationToken { - public static readonly instance = new DefaultHostCancellationToken(); - - public isCancellationRequested() { - return false; - } - } - - export interface LanguageServiceAdapter { - getHost(): LanguageServiceAdapterHost; - getLanguageService(): ts.LanguageService; - getClassifier(): ts.Classifier; - getPreProcessedFileInfo(fileName: string, fileContents: string): ts.PreProcessedFileInfo; - } - - export abstract class LanguageServiceAdapterHost { - public readonly sys = new fakes.System(new vfs.FileSystem(/*ignoreCase*/ true, { cwd: virtualFileSystemRoot })); - public typesRegistry: ts.Map | undefined; - private scriptInfos: collections.SortedMap; - - constructor(protected cancellationToken = DefaultHostCancellationToken.instance, - protected settings = ts.getDefaultCompilerOptions()) { - this.scriptInfos = new collections.SortedMap({ comparer: this.vfs.stringComparer, sort: "insertion" }); - } - - public get vfs() { - return this.sys.vfs; - } - - public getNewLine(): string { - return harnessNewLine; - } - - public getFilenames(): string[] { - const fileNames: string[] = []; - this.scriptInfos.forEach(scriptInfo => { - if (scriptInfo.isRootFile) { - // only include root files here - // usually it means that we won't include lib.d.ts in the list of root files so it won't mess the computation of compilation root dir. - fileNames.push(scriptInfo.fileName); - } - }); - return fileNames; - } - - public getScriptInfo(fileName: string): ScriptInfo | undefined { - return this.scriptInfos.get(vpath.resolve(this.vfs.cwd(), fileName)); - } - - public addScript(fileName: string, content: string, isRootFile: boolean): void { - this.vfs.mkdirpSync(vpath.dirname(fileName)); - this.vfs.writeFileSync(fileName, content); - this.scriptInfos.set(vpath.resolve(this.vfs.cwd(), fileName), new ScriptInfo(fileName, content, isRootFile)); - } - - public renameFileOrDirectory(oldPath: string, newPath: string): void { - this.vfs.mkdirpSync(ts.getDirectoryPath(newPath)); - this.vfs.renameSync(oldPath, newPath); - - const updater = ts.getPathUpdater(oldPath, newPath, ts.createGetCanonicalFileName(this.useCaseSensitiveFileNames()), /*sourceMapper*/ undefined); - this.scriptInfos.forEach((scriptInfo, key) => { - const newFileName = updater(key); - if (newFileName !== undefined) { - this.scriptInfos.delete(key); - this.scriptInfos.set(newFileName, scriptInfo); - scriptInfo.fileName = newFileName; - } - }); - } - - public editScript(fileName: string, start: number, end: number, newText: string) { - const script = this.getScriptInfo(fileName); - if (script) { - script.editContent(start, end, newText); - this.vfs.mkdirpSync(vpath.dirname(fileName)); - this.vfs.writeFileSync(fileName, script.content); - return; - } - - throw new Error("No script with name '" + fileName + "'"); - } - - public openFile(_fileName: string, _content?: string, _scriptKindName?: string): void { /*overridden*/ } - - /** - * @param line 0 based index - * @param col 0 based index - */ - public positionToLineAndCharacter(fileName: string, position: number): ts.LineAndCharacter { - const script: ScriptInfo = this.getScriptInfo(fileName)!; - assert.isOk(script); - return ts.computeLineAndCharacterOfPosition(script.getLineMap(), position); - } - - public lineAndCharacterToPosition(fileName: string, lineAndCharacter: ts.LineAndCharacter): number { - const script: ScriptInfo = this.getScriptInfo(fileName)!; - assert.isOk(script); - return ts.computePositionOfLineAndCharacter(script.getLineMap(), lineAndCharacter.line, lineAndCharacter.character); - } - - useCaseSensitiveFileNames() { - return !this.vfs.ignoreCase; - } - } - - /// Native adapter - class NativeLanguageServiceHost extends LanguageServiceAdapterHost implements ts.LanguageServiceHost, LanguageServiceAdapterHost { - isKnownTypesPackageName(name: string): boolean { - return !!this.typesRegistry && this.typesRegistry.has(name); - } - - getGlobalTypingsCacheLocation() { - return "/Library/Caches/typescript"; - } - - installPackage = ts.notImplemented; - - getCompilationSettings() { return this.settings; } - - getCancellationToken() { return this.cancellationToken; } - - getDirectories(path: string): string[] { - return this.sys.getDirectories(path); - } - - getCurrentDirectory(): string { return virtualFileSystemRoot; } - - getDefaultLibFileName(): string { return Compiler.defaultLibFileName; } - - getScriptFileNames(): string[] { - return this.getFilenames().filter(ts.isAnySupportedFileExtension); - } - - getScriptSnapshot(fileName: string): ts.IScriptSnapshot | undefined { - const script = this.getScriptInfo(fileName); - return script ? new ScriptSnapshot(script) : undefined; - } - - getScriptKind(): ts.ScriptKind { return ts.ScriptKind.Unknown; } - - getScriptVersion(fileName: string): string { - const script = this.getScriptInfo(fileName); - return script ? script.version.toString() : undefined!; // TODO: GH#18217 - } - - directoryExists(dirName: string): boolean { - return this.sys.directoryExists(dirName); - } - - fileExists(fileName: string): boolean { - return this.sys.fileExists(fileName); - } - - readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[] { - return this.sys.readDirectory(path, extensions, exclude, include, depth); - } - - readFile(path: string): string | undefined { - return this.sys.readFile(path); - } - - realpath(path: string): string { - return this.sys.realpath(path); - } - - getTypeRootsVersion() { - return 0; - } - - log = ts.noop; - trace = ts.noop; - error = ts.noop; - } - - export class NativeLanguageServiceAdapter implements LanguageServiceAdapter { - private host: NativeLanguageServiceHost; - constructor(cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) { - this.host = new NativeLanguageServiceHost(cancellationToken, options); - } - getHost(): LanguageServiceAdapterHost { return this.host; } - getLanguageService(): ts.LanguageService { return ts.createLanguageService(this.host); } - getClassifier(): ts.Classifier { return ts.createClassifier(); } - getPreProcessedFileInfo(fileName: string, fileContents: string): ts.PreProcessedFileInfo { return ts.preProcessFile(fileContents, /* readImportFiles */ true, ts.hasJSFileExtension(fileName)); } - } - - /// Shim adapter - class ShimLanguageServiceHost extends LanguageServiceAdapterHost implements ts.LanguageServiceShimHost, ts.CoreServicesShimHost { - private nativeHost: NativeLanguageServiceHost; - - public getModuleResolutionsForFile: ((fileName: string) => string) | undefined; - public getTypeReferenceDirectiveResolutionsForFile: ((fileName: string) => string) | undefined; - - constructor(preprocessToResolve: boolean, cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) { - super(cancellationToken, options); - this.nativeHost = new NativeLanguageServiceHost(cancellationToken, options); - - if (preprocessToResolve) { - const compilerOptions = this.nativeHost.getCompilationSettings(); - const moduleResolutionHost: ts.ModuleResolutionHost = { - fileExists: fileName => this.getScriptInfo(fileName) !== undefined, - readFile: fileName => { - const scriptInfo = this.getScriptInfo(fileName); - return scriptInfo && scriptInfo.content; - } - }; - this.getModuleResolutionsForFile = (fileName) => { - const scriptInfo = this.getScriptInfo(fileName)!; - const preprocessInfo = ts.preProcessFile(scriptInfo.content, /*readImportFiles*/ true); - const imports: ts.MapLike = {}; - for (const module of preprocessInfo.importedFiles) { - const resolutionInfo = ts.resolveModuleName(module.fileName, fileName, compilerOptions, moduleResolutionHost); - if (resolutionInfo.resolvedModule) { - imports[module.fileName] = resolutionInfo.resolvedModule.resolvedFileName; - } - } - return JSON.stringify(imports); - }; - this.getTypeReferenceDirectiveResolutionsForFile = (fileName) => { - const scriptInfo = this.getScriptInfo(fileName); - if (scriptInfo) { - const preprocessInfo = ts.preProcessFile(scriptInfo.content, /*readImportFiles*/ false); - const resolutions: ts.MapLike = {}; - const settings = this.nativeHost.getCompilationSettings(); - for (const typeReferenceDirective of preprocessInfo.typeReferenceDirectives) { - const resolutionInfo = ts.resolveTypeReferenceDirective(typeReferenceDirective.fileName, fileName, settings, moduleResolutionHost); - if (resolutionInfo.resolvedTypeReferenceDirective!.resolvedFileName) { - resolutions[typeReferenceDirective.fileName] = resolutionInfo.resolvedTypeReferenceDirective!; - } - } - return JSON.stringify(resolutions); - } - else { - return "[]"; - } - }; - } - } - - getFilenames(): string[] { return this.nativeHost.getFilenames(); } - getScriptInfo(fileName: string): ScriptInfo | undefined { return this.nativeHost.getScriptInfo(fileName); } - addScript(fileName: string, content: string, isRootFile: boolean): void { this.nativeHost.addScript(fileName, content, isRootFile); } - editScript(fileName: string, start: number, end: number, newText: string): void { this.nativeHost.editScript(fileName, start, end, newText); } - positionToLineAndCharacter(fileName: string, position: number): ts.LineAndCharacter { return this.nativeHost.positionToLineAndCharacter(fileName, position); } - - getCompilationSettings(): string { return JSON.stringify(this.nativeHost.getCompilationSettings()); } - getCancellationToken(): ts.HostCancellationToken { return this.nativeHost.getCancellationToken(); } - getCurrentDirectory(): string { return this.nativeHost.getCurrentDirectory(); } - getDirectories(path: string): string { return JSON.stringify(this.nativeHost.getDirectories(path)); } - getDefaultLibFileName(): string { return this.nativeHost.getDefaultLibFileName(); } - getScriptFileNames(): string { return JSON.stringify(this.nativeHost.getScriptFileNames()); } - getScriptSnapshot(fileName: string): ts.ScriptSnapshotShim { - const nativeScriptSnapshot = this.nativeHost.getScriptSnapshot(fileName)!; // TODO: GH#18217 - return nativeScriptSnapshot && new ScriptSnapshotProxy(nativeScriptSnapshot); - } - getScriptKind(): ts.ScriptKind { return this.nativeHost.getScriptKind(); } - getScriptVersion(fileName: string): string { return this.nativeHost.getScriptVersion(fileName); } - getLocalizedDiagnosticMessages(): string { return JSON.stringify({}); } - - readDirectory = ts.notImplemented; - readDirectoryNames = ts.notImplemented; - readFileNames = ts.notImplemented; - fileExists(fileName: string) { return this.getScriptInfo(fileName) !== undefined; } - readFile(fileName: string) { - const snapshot = this.nativeHost.getScriptSnapshot(fileName); - return snapshot && ts.getSnapshotText(snapshot); - } - log(s: string): void { this.nativeHost.log(s); } - trace(s: string): void { this.nativeHost.trace(s); } - error(s: string): void { this.nativeHost.error(s); } - directoryExists(): boolean { - // for tests pessimistically assume that directory always exists - return true; - } - } - - class ClassifierShimProxy implements ts.Classifier { - constructor(private shim: ts.ClassifierShim) { - } - getEncodedLexicalClassifications(_text: string, _lexState: ts.EndOfLineState, _classifyKeywordsInGenerics?: boolean): ts.Classifications { - return ts.notImplemented(); - } - getClassificationsForLine(text: string, lexState: ts.EndOfLineState, classifyKeywordsInGenerics?: boolean): ts.ClassificationResult { - const result = this.shim.getClassificationsForLine(text, lexState, classifyKeywordsInGenerics).split("\n"); - const entries: ts.ClassificationInfo[] = []; - let i = 0; - let position = 0; - - for (; i < result.length - 1; i += 2) { - const t = entries[i / 2] = { - length: parseInt(result[i]), - classification: parseInt(result[i + 1]) - }; - - assert.isTrue(t.length > 0, "Result length should be greater than 0, got :" + t.length); - position += t.length; - } - const finalLexState = parseInt(result[result.length - 1]); - - assert.equal(position, text.length, "Expected cumulative length of all entries to match the length of the source. expected: " + text.length + ", but got: " + position); - - return { - finalLexState, - entries - }; - } - } - - function unwrapJSONCallResult(result: string): any { - const parsedResult = JSON.parse(result); - if (parsedResult.error) { - throw new Error("Language Service Shim Error: " + JSON.stringify(parsedResult.error)); - } - else if (parsedResult.canceled) { - throw new ts.OperationCanceledException(); - } - return parsedResult.result; - } - - class LanguageServiceShimProxy implements ts.LanguageService { - constructor(private shim: ts.LanguageServiceShim) { - } - cleanupSemanticCache(): void { - this.shim.cleanupSemanticCache(); - } - getSyntacticDiagnostics(fileName: string): ts.DiagnosticWithLocation[] { - return unwrapJSONCallResult(this.shim.getSyntacticDiagnostics(fileName)); - } - getSemanticDiagnostics(fileName: string): ts.DiagnosticWithLocation[] { - return unwrapJSONCallResult(this.shim.getSemanticDiagnostics(fileName)); - } - getSuggestionDiagnostics(fileName: string): ts.DiagnosticWithLocation[] { - return unwrapJSONCallResult(this.shim.getSuggestionDiagnostics(fileName)); - } - getCompilerOptionsDiagnostics(): ts.Diagnostic[] { - return unwrapJSONCallResult(this.shim.getCompilerOptionsDiagnostics()); - } - getSyntacticClassifications(fileName: string, span: ts.TextSpan): ts.ClassifiedSpan[] { - return unwrapJSONCallResult(this.shim.getSyntacticClassifications(fileName, span.start, span.length)); - } - getSemanticClassifications(fileName: string, span: ts.TextSpan): ts.ClassifiedSpan[] { - return unwrapJSONCallResult(this.shim.getSemanticClassifications(fileName, span.start, span.length)); - } - getEncodedSyntacticClassifications(fileName: string, span: ts.TextSpan): ts.Classifications { - return unwrapJSONCallResult(this.shim.getEncodedSyntacticClassifications(fileName, span.start, span.length)); - } - getEncodedSemanticClassifications(fileName: string, span: ts.TextSpan): ts.Classifications { - return unwrapJSONCallResult(this.shim.getEncodedSemanticClassifications(fileName, span.start, span.length)); - } - getCompletionsAtPosition(fileName: string, position: number, preferences: ts.UserPreferences | undefined): ts.CompletionInfo { - return unwrapJSONCallResult(this.shim.getCompletionsAtPosition(fileName, position, preferences)); - } - getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: ts.FormatCodeOptions | undefined, source: string | undefined, preferences: ts.UserPreferences | undefined): ts.CompletionEntryDetails { - return unwrapJSONCallResult(this.shim.getCompletionEntryDetails(fileName, position, entryName, JSON.stringify(formatOptions), source, preferences)); - } - getCompletionEntrySymbol(): ts.Symbol { - throw new Error("getCompletionEntrySymbol not implemented across the shim layer."); - } - getQuickInfoAtPosition(fileName: string, position: number): ts.QuickInfo { - return unwrapJSONCallResult(this.shim.getQuickInfoAtPosition(fileName, position)); - } - getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): ts.TextSpan { - return unwrapJSONCallResult(this.shim.getNameOrDottedNameSpan(fileName, startPos, endPos)); - } - getBreakpointStatementAtPosition(fileName: string, position: number): ts.TextSpan { - return unwrapJSONCallResult(this.shim.getBreakpointStatementAtPosition(fileName, position)); - } - getSignatureHelpItems(fileName: string, position: number, options: ts.SignatureHelpItemsOptions | undefined): ts.SignatureHelpItems { - return unwrapJSONCallResult(this.shim.getSignatureHelpItems(fileName, position, options)); - } - getRenameInfo(fileName: string, position: number, options?: ts.RenameInfoOptions): ts.RenameInfo { - return unwrapJSONCallResult(this.shim.getRenameInfo(fileName, position, options)); - } - getSmartSelectionRange(fileName: string, position: number): ts.SelectionRange { - return unwrapJSONCallResult(this.shim.getSmartSelectionRange(fileName, position)); - } - findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): ts.RenameLocation[] { - return unwrapJSONCallResult(this.shim.findRenameLocations(fileName, position, findInStrings, findInComments, providePrefixAndSuffixTextForRename)); - } - getDefinitionAtPosition(fileName: string, position: number): ts.DefinitionInfo[] { - return unwrapJSONCallResult(this.shim.getDefinitionAtPosition(fileName, position)); - } - getDefinitionAndBoundSpan(fileName: string, position: number): ts.DefinitionInfoAndBoundSpan { - return unwrapJSONCallResult(this.shim.getDefinitionAndBoundSpan(fileName, position)); - } - getTypeDefinitionAtPosition(fileName: string, position: number): ts.DefinitionInfo[] { - return unwrapJSONCallResult(this.shim.getTypeDefinitionAtPosition(fileName, position)); - } - getImplementationAtPosition(fileName: string, position: number): ts.ImplementationLocation[] { - return unwrapJSONCallResult(this.shim.getImplementationAtPosition(fileName, position)); - } - getReferencesAtPosition(fileName: string, position: number): ts.ReferenceEntry[] { - return unwrapJSONCallResult(this.shim.getReferencesAtPosition(fileName, position)); - } - findReferences(fileName: string, position: number): ts.ReferencedSymbol[] { - return unwrapJSONCallResult(this.shim.findReferences(fileName, position)); - } - getOccurrencesAtPosition(fileName: string, position: number): ts.ReferenceEntry[] { - return unwrapJSONCallResult(this.shim.getOccurrencesAtPosition(fileName, position)); - } - getDocumentHighlights(fileName: string, position: number, filesToSearch: string[]): ts.DocumentHighlights[] { - return unwrapJSONCallResult(this.shim.getDocumentHighlights(fileName, position, JSON.stringify(filesToSearch))); - } - getNavigateToItems(searchValue: string): ts.NavigateToItem[] { - return unwrapJSONCallResult(this.shim.getNavigateToItems(searchValue)); - } - getNavigationBarItems(fileName: string): ts.NavigationBarItem[] { - return unwrapJSONCallResult(this.shim.getNavigationBarItems(fileName)); - } - getNavigationTree(fileName: string): ts.NavigationTree { - return unwrapJSONCallResult(this.shim.getNavigationTree(fileName)); - } - getOutliningSpans(fileName: string): ts.OutliningSpan[] { - return unwrapJSONCallResult(this.shim.getOutliningSpans(fileName)); - } - getTodoComments(fileName: string, descriptors: ts.TodoCommentDescriptor[]): ts.TodoComment[] { - return unwrapJSONCallResult(this.shim.getTodoComments(fileName, JSON.stringify(descriptors))); - } - getBraceMatchingAtPosition(fileName: string, position: number): ts.TextSpan[] { - return unwrapJSONCallResult(this.shim.getBraceMatchingAtPosition(fileName, position)); - } - getIndentationAtPosition(fileName: string, position: number, options: ts.EditorOptions): number { - return unwrapJSONCallResult(this.shim.getIndentationAtPosition(fileName, position, JSON.stringify(options))); - } - getFormattingEditsForRange(fileName: string, start: number, end: number, options: ts.FormatCodeOptions): ts.TextChange[] { - return unwrapJSONCallResult(this.shim.getFormattingEditsForRange(fileName, start, end, JSON.stringify(options))); - } - getFormattingEditsForDocument(fileName: string, options: ts.FormatCodeOptions): ts.TextChange[] { - return unwrapJSONCallResult(this.shim.getFormattingEditsForDocument(fileName, JSON.stringify(options))); - } - getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: ts.FormatCodeOptions): ts.TextChange[] { - return unwrapJSONCallResult(this.shim.getFormattingEditsAfterKeystroke(fileName, position, key, JSON.stringify(options))); - } - getDocCommentTemplateAtPosition(fileName: string, position: number): ts.TextInsertion { - return unwrapJSONCallResult(this.shim.getDocCommentTemplateAtPosition(fileName, position)); - } - isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean { - return unwrapJSONCallResult(this.shim.isValidBraceCompletionAtPosition(fileName, position, openingBrace)); - } - getJsxClosingTagAtPosition(): never { - throw new Error("Not supported on the shim."); - } - getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): ts.TextSpan { - return unwrapJSONCallResult(this.shim.getSpanOfEnclosingComment(fileName, position, onlyMultiLine)); - } - getCodeFixesAtPosition(): never { - throw new Error("Not supported on the shim."); - } - getCombinedCodeFix = ts.notImplemented; - applyCodeActionCommand = ts.notImplemented; - getCodeFixDiagnostics(): ts.Diagnostic[] { - throw new Error("Not supported on the shim."); - } - getEditsForRefactor(): ts.RefactorEditInfo { - throw new Error("Not supported on the shim."); - } - getApplicableRefactors(): ts.ApplicableRefactorInfo[] { - throw new Error("Not supported on the shim."); - } - organizeImports(_scope: ts.OrganizeImportsScope, _formatOptions: ts.FormatCodeSettings): readonly ts.FileTextChanges[] { - throw new Error("Not supported on the shim."); - } - getEditsForFileRename(): readonly ts.FileTextChanges[] { - throw new Error("Not supported on the shim."); - } - prepareCallHierarchy(fileName: string, position: number) { - return unwrapJSONCallResult(this.shim.prepareCallHierarchy(fileName, position)); - } - provideCallHierarchyIncomingCalls(fileName: string, position: number) { - return unwrapJSONCallResult(this.shim.provideCallHierarchyIncomingCalls(fileName, position)); - } - provideCallHierarchyOutgoingCalls(fileName: string, position: number) { - return unwrapJSONCallResult(this.shim.provideCallHierarchyOutgoingCalls(fileName, position)); - } - getEmitOutput(fileName: string): ts.EmitOutput { - return unwrapJSONCallResult(this.shim.getEmitOutput(fileName)); - } - getProgram(): ts.Program { - throw new Error("Program can not be marshaled across the shim layer."); - } - getNonBoundSourceFile(): ts.SourceFile { - throw new Error("SourceFile can not be marshaled across the shim layer."); - } - getSourceFile(): ts.SourceFile { - throw new Error("SourceFile can not be marshaled across the shim layer."); - } - getSourceMapper(): never { - return ts.notImplemented(); - } - clearSourceMapperCache(): never { - return ts.notImplemented(); - } - toggleLineComment(fileName: string, textRange: ts.TextRange): ts.TextChange[] { - return unwrapJSONCallResult(this.shim.toggleLineComment(fileName, textRange)); - } - toggleMultilineComment(fileName: string, textRange: ts.TextRange): ts.TextChange[] { - return unwrapJSONCallResult(this.shim.toggleMultilineComment(fileName, textRange)); - } - commentSelection(fileName: string, textRange: ts.TextRange): ts.TextChange[] { - return unwrapJSONCallResult(this.shim.commentSelection(fileName, textRange)); - } - uncommentSelection(fileName: string, textRange: ts.TextRange): ts.TextChange[] { - return unwrapJSONCallResult(this.shim.uncommentSelection(fileName, textRange)); - } - dispose(): void { this.shim.dispose({}); } - } - - export class ShimLanguageServiceAdapter implements LanguageServiceAdapter { - private host: ShimLanguageServiceHost; - private factory: ts.TypeScriptServicesFactory; - constructor(preprocessToResolve: boolean, cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) { - this.host = new ShimLanguageServiceHost(preprocessToResolve, cancellationToken, options); - this.factory = new ts.TypeScriptServicesFactory(); - } - getHost() { return this.host; } - getLanguageService(): ts.LanguageService { return new LanguageServiceShimProxy(this.factory.createLanguageServiceShim(this.host)); } - getClassifier(): ts.Classifier { return new ClassifierShimProxy(this.factory.createClassifierShim(this.host)); } - getPreProcessedFileInfo(fileName: string, fileContents: string): ts.PreProcessedFileInfo { - const coreServicesShim = this.factory.createCoreServicesShim(this.host); - const shimResult: { - referencedFiles: ts.ShimsFileReference[]; - typeReferenceDirectives: ts.ShimsFileReference[]; - importedFiles: ts.ShimsFileReference[]; - isLibFile: boolean; - } = unwrapJSONCallResult(coreServicesShim.getPreProcessedFileInfo(fileName, ts.ScriptSnapshot.fromString(fileContents))); - - const convertResult: ts.PreProcessedFileInfo = { - referencedFiles: [], - importedFiles: [], - ambientExternalModules: [], - isLibFile: shimResult.isLibFile, - typeReferenceDirectives: [], - libReferenceDirectives: [] - }; - - ts.forEach(shimResult.referencedFiles, refFile => { - convertResult.referencedFiles.push({ - fileName: refFile.path, - pos: refFile.position, - end: refFile.position + refFile.length - }); - }); - - ts.forEach(shimResult.importedFiles, importedFile => { - convertResult.importedFiles.push({ - fileName: importedFile.path, - pos: importedFile.position, - end: importedFile.position + importedFile.length - }); - }); - - ts.forEach(shimResult.typeReferenceDirectives, typeRefDirective => { - convertResult.importedFiles.push({ - fileName: typeRefDirective.path, - pos: typeRefDirective.position, - end: typeRefDirective.position + typeRefDirective.length - }); - }); - return convertResult; - } - } - - // Server adapter - class SessionClientHost extends NativeLanguageServiceHost implements ts.server.SessionClientHost { - private client!: ts.server.SessionClient; - - constructor(cancellationToken: ts.HostCancellationToken | undefined, settings: ts.CompilerOptions | undefined) { - super(cancellationToken, settings); - } - - onMessage = ts.noop; - writeMessage = ts.noop; - - setClient(client: ts.server.SessionClient) { - this.client = client; - } - - openFile(fileName: string, content?: string, scriptKindName?: "TS" | "JS" | "TSX" | "JSX"): void { - super.openFile(fileName, content, scriptKindName); - this.client.openFile(fileName, content, scriptKindName); - } - - editScript(fileName: string, start: number, end: number, newText: string) { - const changeArgs = this.client.createChangeFileRequestArgs(fileName, start, end, newText); - super.editScript(fileName, start, end, newText); - this.client.changeFile(fileName, changeArgs); - } - } - - class SessionServerHost implements ts.server.ServerHost, ts.server.Logger { - args: string[] = []; - newLine: string; - useCaseSensitiveFileNames = false; - - constructor(private host: NativeLanguageServiceHost) { - this.newLine = this.host.getNewLine(); - } - - onMessage = ts.noop; - writeMessage = ts.noop; // overridden - write(message: string): void { - this.writeMessage(message); - } - - readFile(fileName: string): string | undefined { - if (ts.stringContains(fileName, Compiler.defaultLibFileName)) { - fileName = Compiler.defaultLibFileName; - } - - const snapshot = this.host.getScriptSnapshot(fileName); - return snapshot && ts.getSnapshotText(snapshot); - } - - writeFile = ts.noop; - - resolvePath(path: string): string { - return path; - } - - fileExists(path: string): boolean { - return !!this.host.getScriptSnapshot(path); - } - - directoryExists(): boolean { - // for tests assume that directory exists - return true; - } - - getExecutingFilePath(): string { - return ""; - } - - exit = ts.noop; - - createDirectory(_directoryName: string): void { - return ts.notImplemented(); - } - - getCurrentDirectory(): string { - return this.host.getCurrentDirectory(); - } - - getDirectories(path: string): string[] { - return this.host.getDirectories(path); - } - - getEnvironmentVariable(name: string): string { - return ts.sys.getEnvironmentVariable(name); - } - - readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[] { - return this.host.readDirectory(path, extensions, exclude, include, depth); - } - - watchFile(): ts.FileWatcher { - return { close: ts.noop }; - } - - watchDirectory(): ts.FileWatcher { - return { close: ts.noop }; - } - - close = ts.noop; - - info(message: string): void { - this.host.log(message); - } - - msg(message: string): void { - this.host.log(message); - } - - loggingEnabled() { - return true; - } - - getLogFileName(): string | undefined { - return undefined; - } - - hasLevel() { - return false; - } - - startGroup() { throw ts.notImplemented(); } - endGroup() { throw ts.notImplemented(); } - - perftrc(message: string): void { - return this.host.log(message); - } - - setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): any { - // eslint-disable-next-line no-restricted-globals - return setTimeout(callback, ms, args); - } - - clearTimeout(timeoutId: any): void { - // eslint-disable-next-line no-restricted-globals - clearTimeout(timeoutId); - } - - setImmediate(callback: (...args: any[]) => void, _ms: number, ...args: any[]): any { - // eslint-disable-next-line no-restricted-globals - return setImmediate(callback, args); - } - - clearImmediate(timeoutId: any): void { - // eslint-disable-next-line no-restricted-globals - clearImmediate(timeoutId); - } - - createHash(s: string) { - return mockHash(s); - } - - require(_initialDir: string, _moduleName: string): ts.RequireResult { - switch (_moduleName) { - // Adds to the Quick Info a fixed string and a string from the config file - // and replaces the first display part - case "quickinfo-augmeneter": - return { - module: () => ({ - create(info: ts.server.PluginCreateInfo) { - const proxy = makeDefaultProxy(info); - const langSvc: any = info.languageService; - // eslint-disable-next-line only-arrow-functions - proxy.getQuickInfoAtPosition = function () { - const parts = langSvc.getQuickInfoAtPosition.apply(langSvc, arguments); - if (parts.displayParts.length > 0) { - parts.displayParts[0].text = "Proxied"; - } - parts.displayParts.push({ text: info.config.message, kind: "punctuation" }); - return parts; - }; - - return proxy; - } - }), - error: undefined - }; - - // Throws during initialization - case "create-thrower": - return { - module: () => ({ - create() { - throw new Error("I am not a well-behaved plugin"); - } - }), - error: undefined - }; - - // Adds another diagnostic - case "diagnostic-adder": - return { - module: () => ({ - create(info: ts.server.PluginCreateInfo) { - const proxy = makeDefaultProxy(info); - proxy.getSemanticDiagnostics = filename => { - const prev = info.languageService.getSemanticDiagnostics(filename); - const sourceFile: ts.SourceFile = info.project.getSourceFile(ts.toPath(filename, /*basePath*/ undefined, ts.createGetCanonicalFileName(info.serverHost.useCaseSensitiveFileNames)))!; - prev.push({ - category: ts.DiagnosticCategory.Warning, - file: sourceFile, - code: 9999, - length: 3, - messageText: `Plugin diagnostic`, - start: 0 - }); - return prev; - }; - return proxy; - } - }), - error: undefined - }; - - // Accepts configurations - case "configurable-diagnostic-adder": - let customMessage = "default message"; - return { - module: () => ({ - create(info: ts.server.PluginCreateInfo) { - customMessage = info.config.message; - const proxy = makeDefaultProxy(info); - proxy.getSemanticDiagnostics = filename => { - const prev = info.languageService.getSemanticDiagnostics(filename); - const sourceFile: ts.SourceFile = info.project.getSourceFile(ts.toPath(filename, /*basePath*/ undefined, ts.createGetCanonicalFileName(info.serverHost.useCaseSensitiveFileNames)))!; - prev.push({ - category: ts.DiagnosticCategory.Error, - file: sourceFile, - code: 9999, - length: 3, - messageText: customMessage, - start: 0 - }); - return prev; - }; - return proxy; - }, - onConfigurationChanged(config: any) { - customMessage = config.message; - } - }), - error: undefined - }; - - default: - return { - module: undefined, - error: new Error("Could not resolve module") - }; - } - } - } - - class FourslashSession extends ts.server.Session { - getText(fileName: string) { - return ts.getSnapshotText(this.projectService.getDefaultProjectForFile(ts.server.toNormalizedPath(fileName), /*ensureProject*/ true)!.getScriptSnapshot(fileName)!); - } - } - - export class ServerLanguageServiceAdapter implements LanguageServiceAdapter { - private host: SessionClientHost; - private client: ts.server.SessionClient; - private server: FourslashSession; - constructor(cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) { - // This is the main host that tests use to direct tests - const clientHost = new SessionClientHost(cancellationToken, options); - const client = new ts.server.SessionClient(clientHost); - - // This host is just a proxy for the clientHost, it uses the client - // host to answer server queries about files on disk - const serverHost = new SessionServerHost(clientHost); - const opts: ts.server.SessionOptions = { - host: serverHost, - cancellationToken: ts.server.nullCancellationToken, - useSingleInferredProject: false, - useInferredProjectPerProjectRoot: false, - typingsInstaller: undefined!, // TODO: GH#18217 - byteLength: Utils.byteLength, - hrtime: process.hrtime, - logger: serverHost, - canUseEvents: true - }; - this.server = new FourslashSession(opts); - - - // Fake the connection between the client and the server - serverHost.writeMessage = client.onMessage.bind(client); - clientHost.writeMessage = this.server.onMessage.bind(this.server); - - // Wire the client to the host to get notifications when a file is open - // or edited. - clientHost.setClient(client); - - // Set the properties - this.client = client; - this.host = clientHost; - } - getHost() { return this.host; } - getLanguageService(): ts.LanguageService { return this.client; } - getClassifier(): ts.Classifier { throw new Error("getClassifier is not available using the server interface."); } - getPreProcessedFileInfo(): ts.PreProcessedFileInfo { throw new Error("getPreProcessedFileInfo is not available using the server interface."); } - assertTextConsistent(fileName: string) { - const serverText = this.server.getText(fileName); - const clientText = this.host.readFile(fileName); - ts.Debug.assert(serverText === clientText, [ - "Server and client text are inconsistent.", - "", - "\x1b[1mServer\x1b[0m\x1b[31m:", - serverText, - "", - "\x1b[1mClient\x1b[0m\x1b[31m:", - clientText, - "", - "This probably means something is wrong with the fourslash infrastructure, not with the test." - ].join(ts.sys.newLine)); - } - } -} +namespace Harness.LanguageService { + + export function makeDefaultProxy(info: ts.server.PluginCreateInfo): ts.LanguageService { + const proxy = Object.create(/*prototype*/ null); // eslint-disable-line no-null/no-null + const langSvc: any = info.languageService; + for (const k of Object.keys(langSvc)) { + // eslint-disable-next-line only-arrow-functions + proxy[k] = function () { + return langSvc[k].apply(langSvc, arguments); + }; + } + return proxy; + } + + export class ScriptInfo { + public version = 1; + public editRanges: { length: number; textChangeRange: ts.TextChangeRange; }[] = []; + private lineMap: number[] | undefined; + + constructor(public fileName: string, public content: string, public isRootFile: boolean) { + this.setContent(content); + } + + private setContent(content: string): void { + this.content = content; + this.lineMap = undefined; + } + + public getLineMap(): number[] { + return this.lineMap || (this.lineMap = ts.computeLineStarts(this.content)); + } + + public updateContent(content: string): void { + this.editRanges = []; + this.setContent(content); + this.version++; + } + + public editContent(start: number, end: number, newText: string): void { + // Apply edits + const prefix = this.content.substring(0, start); + const middle = newText; + const suffix = this.content.substring(end); + this.setContent(prefix + middle + suffix); + + // Store edit range + new length of script + this.editRanges.push({ + length: this.content.length, + textChangeRange: ts.createTextChangeRange( + ts.createTextSpanFromBounds(start, end), newText.length) + }); + + // Update version # + this.version++; + } + + public getTextChangeRangeBetweenVersions(startVersion: number, endVersion: number): ts.TextChangeRange { + if (startVersion === endVersion) { + // No edits! + return ts.unchangedTextChangeRange; + } + + const initialEditRangeIndex = this.editRanges.length - (this.version - startVersion); + const lastEditRangeIndex = this.editRanges.length - (this.version - endVersion); + + const entries = this.editRanges.slice(initialEditRangeIndex, lastEditRangeIndex); + return ts.collapseTextChangeRangesAcrossMultipleVersions(entries.map(e => e.textChangeRange)); + } + } + + class ScriptSnapshot implements ts.IScriptSnapshot { + public textSnapshot: string; + public version: number; + + constructor(public scriptInfo: ScriptInfo) { + this.textSnapshot = scriptInfo.content; + this.version = scriptInfo.version; + } + + public getText(start: number, end: number): string { + return this.textSnapshot.substring(start, end); + } + + public getLength(): number { + return this.textSnapshot.length; + } + + public getChangeRange(oldScript: ts.IScriptSnapshot): ts.TextChangeRange { + const oldShim = oldScript; + return this.scriptInfo.getTextChangeRangeBetweenVersions(oldShim.version, this.version); + } + } + + class ScriptSnapshotProxy implements ts.ScriptSnapshotShim { + constructor(private readonly scriptSnapshot: ts.IScriptSnapshot) { + } + + public getText(start: number, end: number): string { + return this.scriptSnapshot.getText(start, end); + } + + public getLength(): number { + return this.scriptSnapshot.getLength(); + } + + public getChangeRange(oldScript: ts.ScriptSnapshotShim): string | undefined { + const range = this.scriptSnapshot.getChangeRange((oldScript as ScriptSnapshotProxy).scriptSnapshot); + return range && JSON.stringify(range); + } + } + + class DefaultHostCancellationToken implements ts.HostCancellationToken { + public static readonly instance = new DefaultHostCancellationToken(); + + public isCancellationRequested() { + return false; + } + } + + export interface LanguageServiceAdapter { + getHost(): LanguageServiceAdapterHost; + getLanguageService(): ts.LanguageService; + getClassifier(): ts.Classifier; + getPreProcessedFileInfo(fileName: string, fileContents: string): ts.PreProcessedFileInfo; + } + + export abstract class LanguageServiceAdapterHost { + public readonly sys = new fakes.System(new vfs.FileSystem(/*ignoreCase*/ true, { cwd: virtualFileSystemRoot })); + public typesRegistry: ts.Map | undefined; + private scriptInfos: collections.SortedMap; + + constructor(protected cancellationToken = DefaultHostCancellationToken.instance, + protected settings = ts.getDefaultCompilerOptions()) { + this.scriptInfos = new collections.SortedMap({ comparer: this.vfs.stringComparer, sort: "insertion" }); + } + + public get vfs() { + return this.sys.vfs; + } + + public getNewLine(): string { + return harnessNewLine; + } + + public getFilenames(): string[] { + const fileNames: string[] = []; + this.scriptInfos.forEach(scriptInfo => { + if (scriptInfo.isRootFile) { + // only include root files here + // usually it means that we won't include lib.d.ts in the list of root files so it won't mess the computation of compilation root dir. + fileNames.push(scriptInfo.fileName); + } + }); + return fileNames; + } + + public getScriptInfo(fileName: string): ScriptInfo | undefined { + return this.scriptInfos.get(vpath.resolve(this.vfs.cwd(), fileName)); + } + + public addScript(fileName: string, content: string, isRootFile: boolean): void { + this.vfs.mkdirpSync(vpath.dirname(fileName)); + this.vfs.writeFileSync(fileName, content); + this.scriptInfos.set(vpath.resolve(this.vfs.cwd(), fileName), new ScriptInfo(fileName, content, isRootFile)); + } + + public renameFileOrDirectory(oldPath: string, newPath: string): void { + this.vfs.mkdirpSync(ts.getDirectoryPath(newPath)); + this.vfs.renameSync(oldPath, newPath); + + const updater = ts.getPathUpdater(oldPath, newPath, ts.createGetCanonicalFileName(this.useCaseSensitiveFileNames()), /*sourceMapper*/ undefined); + this.scriptInfos.forEach((scriptInfo, key) => { + const newFileName = updater(key); + if (newFileName !== undefined) { + this.scriptInfos.delete(key); + this.scriptInfos.set(newFileName, scriptInfo); + scriptInfo.fileName = newFileName; + } + }); + } + + public editScript(fileName: string, start: number, end: number, newText: string) { + const script = this.getScriptInfo(fileName); + if (script) { + script.editContent(start, end, newText); + this.vfs.mkdirpSync(vpath.dirname(fileName)); + this.vfs.writeFileSync(fileName, script.content); + return; + } + + throw new Error("No script with name '" + fileName + "'"); + } + + public openFile(_fileName: string, _content?: string, _scriptKindName?: string): void { /*overridden*/ } + + /** + * @param line 0 based index + * @param col 0 based index + */ + public positionToLineAndCharacter(fileName: string, position: number): ts.LineAndCharacter { + const script: ScriptInfo = this.getScriptInfo(fileName)!; + assert.isOk(script); + return ts.computeLineAndCharacterOfPosition(script.getLineMap(), position); + } + + public lineAndCharacterToPosition(fileName: string, lineAndCharacter: ts.LineAndCharacter): number { + const script: ScriptInfo = this.getScriptInfo(fileName)!; + assert.isOk(script); + return ts.computePositionOfLineAndCharacter(script.getLineMap(), lineAndCharacter.line, lineAndCharacter.character); + } + + useCaseSensitiveFileNames() { + return !this.vfs.ignoreCase; + } + } + + /// Native adapter + class NativeLanguageServiceHost extends LanguageServiceAdapterHost implements ts.LanguageServiceHost, LanguageServiceAdapterHost { + isKnownTypesPackageName(name: string): boolean { + return !!this.typesRegistry && this.typesRegistry.has(name); + } + + getGlobalTypingsCacheLocation() { + return "/Library/Caches/typescript"; + } + + installPackage = ts.notImplemented; + + getCompilationSettings() { return this.settings; } + + getCancellationToken() { return this.cancellationToken; } + + getDirectories(path: string): string[] { + return this.sys.getDirectories(path); + } + + getCurrentDirectory(): string { return virtualFileSystemRoot; } + + getDefaultLibFileName(): string { return Compiler.defaultLibFileName; } + + getScriptFileNames(): string[] { + return this.getFilenames().filter(ts.isAnySupportedFileExtension); + } + + getScriptSnapshot(fileName: string): ts.IScriptSnapshot | undefined { + const script = this.getScriptInfo(fileName); + return script ? new ScriptSnapshot(script) : undefined; + } + + getScriptKind(): ts.ScriptKind { return ts.ScriptKind.Unknown; } + + getScriptVersion(fileName: string): string { + const script = this.getScriptInfo(fileName); + return script ? script.version.toString() : undefined!; // TODO: GH#18217 + } + + directoryExists(dirName: string): boolean { + return this.sys.directoryExists(dirName); + } + + fileExists(fileName: string): boolean { + return this.sys.fileExists(fileName); + } + + readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[] { + return this.sys.readDirectory(path, extensions, exclude, include, depth); + } + + readFile(path: string): string | undefined { + return this.sys.readFile(path); + } + + realpath(path: string): string { + return this.sys.realpath(path); + } + + getTypeRootsVersion() { + return 0; + } + + log = ts.noop; + trace = ts.noop; + error = ts.noop; + } + + export class NativeLanguageServiceAdapter implements LanguageServiceAdapter { + private host: NativeLanguageServiceHost; + constructor(cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) { + this.host = new NativeLanguageServiceHost(cancellationToken, options); + } + getHost(): LanguageServiceAdapterHost { return this.host; } + getLanguageService(): ts.LanguageService { return ts.createLanguageService(this.host); } + getClassifier(): ts.Classifier { return ts.createClassifier(); } + getPreProcessedFileInfo(fileName: string, fileContents: string): ts.PreProcessedFileInfo { return ts.preProcessFile(fileContents, /* readImportFiles */ true, ts.hasJSFileExtension(fileName)); } + } + + /// Shim adapter + class ShimLanguageServiceHost extends LanguageServiceAdapterHost implements ts.LanguageServiceShimHost, ts.CoreServicesShimHost { + private nativeHost: NativeLanguageServiceHost; + + public getModuleResolutionsForFile: ((fileName: string) => string) | undefined; + public getTypeReferenceDirectiveResolutionsForFile: ((fileName: string) => string) | undefined; + + constructor(preprocessToResolve: boolean, cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) { + super(cancellationToken, options); + this.nativeHost = new NativeLanguageServiceHost(cancellationToken, options); + + if (preprocessToResolve) { + const compilerOptions = this.nativeHost.getCompilationSettings(); + const moduleResolutionHost: ts.ModuleResolutionHost = { + fileExists: fileName => this.getScriptInfo(fileName) !== undefined, + readFile: fileName => { + const scriptInfo = this.getScriptInfo(fileName); + return scriptInfo && scriptInfo.content; + } + }; + this.getModuleResolutionsForFile = (fileName) => { + const scriptInfo = this.getScriptInfo(fileName)!; + const preprocessInfo = ts.preProcessFile(scriptInfo.content, /*readImportFiles*/ true); + const imports: ts.MapLike = {}; + for (const module of preprocessInfo.importedFiles) { + const resolutionInfo = ts.resolveModuleName(module.fileName, fileName, compilerOptions, moduleResolutionHost); + if (resolutionInfo.resolvedModule) { + imports[module.fileName] = resolutionInfo.resolvedModule.resolvedFileName; + } + } + return JSON.stringify(imports); + }; + this.getTypeReferenceDirectiveResolutionsForFile = (fileName) => { + const scriptInfo = this.getScriptInfo(fileName); + if (scriptInfo) { + const preprocessInfo = ts.preProcessFile(scriptInfo.content, /*readImportFiles*/ false); + const resolutions: ts.MapLike = {}; + const settings = this.nativeHost.getCompilationSettings(); + for (const typeReferenceDirective of preprocessInfo.typeReferenceDirectives) { + const resolutionInfo = ts.resolveTypeReferenceDirective(typeReferenceDirective.fileName, fileName, settings, moduleResolutionHost); + if (resolutionInfo.resolvedTypeReferenceDirective!.resolvedFileName) { + resolutions[typeReferenceDirective.fileName] = resolutionInfo.resolvedTypeReferenceDirective!; + } + } + return JSON.stringify(resolutions); + } + else { + return "[]"; + } + }; + } + } + + getFilenames(): string[] { return this.nativeHost.getFilenames(); } + getScriptInfo(fileName: string): ScriptInfo | undefined { return this.nativeHost.getScriptInfo(fileName); } + addScript(fileName: string, content: string, isRootFile: boolean): void { this.nativeHost.addScript(fileName, content, isRootFile); } + editScript(fileName: string, start: number, end: number, newText: string): void { this.nativeHost.editScript(fileName, start, end, newText); } + positionToLineAndCharacter(fileName: string, position: number): ts.LineAndCharacter { return this.nativeHost.positionToLineAndCharacter(fileName, position); } + + getCompilationSettings(): string { return JSON.stringify(this.nativeHost.getCompilationSettings()); } + getCancellationToken(): ts.HostCancellationToken { return this.nativeHost.getCancellationToken(); } + getCurrentDirectory(): string { return this.nativeHost.getCurrentDirectory(); } + getDirectories(path: string): string { return JSON.stringify(this.nativeHost.getDirectories(path)); } + getDefaultLibFileName(): string { return this.nativeHost.getDefaultLibFileName(); } + getScriptFileNames(): string { return JSON.stringify(this.nativeHost.getScriptFileNames()); } + getScriptSnapshot(fileName: string): ts.ScriptSnapshotShim { + const nativeScriptSnapshot = this.nativeHost.getScriptSnapshot(fileName)!; // TODO: GH#18217 + return nativeScriptSnapshot && new ScriptSnapshotProxy(nativeScriptSnapshot); + } + getScriptKind(): ts.ScriptKind { return this.nativeHost.getScriptKind(); } + getScriptVersion(fileName: string): string { return this.nativeHost.getScriptVersion(fileName); } + getLocalizedDiagnosticMessages(): string { return JSON.stringify({}); } + + readDirectory = ts.notImplemented; + readDirectoryNames = ts.notImplemented; + readFileNames = ts.notImplemented; + fileExists(fileName: string) { return this.getScriptInfo(fileName) !== undefined; } + readFile(fileName: string) { + const snapshot = this.nativeHost.getScriptSnapshot(fileName); + return snapshot && ts.getSnapshotText(snapshot); + } + log(s: string): void { this.nativeHost.log(s); } + trace(s: string): void { this.nativeHost.trace(s); } + error(s: string): void { this.nativeHost.error(s); } + directoryExists(): boolean { + // for tests pessimistically assume that directory always exists + return true; + } + } + + class ClassifierShimProxy implements ts.Classifier { + constructor(private shim: ts.ClassifierShim) { + } + getEncodedLexicalClassifications(_text: string, _lexState: ts.EndOfLineState, _classifyKeywordsInGenerics?: boolean): ts.Classifications { + return ts.notImplemented(); + } + getClassificationsForLine(text: string, lexState: ts.EndOfLineState, classifyKeywordsInGenerics?: boolean): ts.ClassificationResult { + const result = this.shim.getClassificationsForLine(text, lexState, classifyKeywordsInGenerics).split("\n"); + const entries: ts.ClassificationInfo[] = []; + let i = 0; + let position = 0; + + for (; i < result.length - 1; i += 2) { + const t = entries[i / 2] = { + length: parseInt(result[i]), + classification: parseInt(result[i + 1]) + }; + + assert.isTrue(t.length > 0, "Result length should be greater than 0, got :" + t.length); + position += t.length; + } + const finalLexState = parseInt(result[result.length - 1]); + + assert.equal(position, text.length, "Expected cumulative length of all entries to match the length of the source. expected: " + text.length + ", but got: " + position); + + return { + finalLexState, + entries + }; + } + } + + function unwrapJSONCallResult(result: string): any { + const parsedResult = JSON.parse(result); + if (parsedResult.error) { + throw new Error("Language Service Shim Error: " + JSON.stringify(parsedResult.error)); + } + else if (parsedResult.canceled) { + throw new ts.OperationCanceledException(); + } + return parsedResult.result; + } + + class LanguageServiceShimProxy implements ts.LanguageService { + constructor(private shim: ts.LanguageServiceShim) { + } + cleanupSemanticCache(): void { + this.shim.cleanupSemanticCache(); + } + getSyntacticDiagnostics(fileName: string): ts.DiagnosticWithLocation[] { + return unwrapJSONCallResult(this.shim.getSyntacticDiagnostics(fileName)); + } + getSemanticDiagnostics(fileName: string): ts.DiagnosticWithLocation[] { + return unwrapJSONCallResult(this.shim.getSemanticDiagnostics(fileName)); + } + getSuggestionDiagnostics(fileName: string): ts.DiagnosticWithLocation[] { + return unwrapJSONCallResult(this.shim.getSuggestionDiagnostics(fileName)); + } + getCompilerOptionsDiagnostics(): ts.Diagnostic[] { + return unwrapJSONCallResult(this.shim.getCompilerOptionsDiagnostics()); + } + getSyntacticClassifications(fileName: string, span: ts.TextSpan): ts.ClassifiedSpan[] { + return unwrapJSONCallResult(this.shim.getSyntacticClassifications(fileName, span.start, span.length)); + } + getSemanticClassifications(fileName: string, span: ts.TextSpan): ts.ClassifiedSpan[] { + return unwrapJSONCallResult(this.shim.getSemanticClassifications(fileName, span.start, span.length)); + } + getEncodedSyntacticClassifications(fileName: string, span: ts.TextSpan): ts.Classifications { + return unwrapJSONCallResult(this.shim.getEncodedSyntacticClassifications(fileName, span.start, span.length)); + } + getEncodedSemanticClassifications(fileName: string, span: ts.TextSpan): ts.Classifications { + return unwrapJSONCallResult(this.shim.getEncodedSemanticClassifications(fileName, span.start, span.length)); + } + getCompletionsAtPosition(fileName: string, position: number, preferences: ts.UserPreferences | undefined): ts.CompletionInfo { + return unwrapJSONCallResult(this.shim.getCompletionsAtPosition(fileName, position, preferences)); + } + getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: ts.FormatCodeOptions | undefined, source: string | undefined, preferences: ts.UserPreferences | undefined): ts.CompletionEntryDetails { + return unwrapJSONCallResult(this.shim.getCompletionEntryDetails(fileName, position, entryName, JSON.stringify(formatOptions), source, preferences)); + } + getCompletionEntrySymbol(): ts.Symbol { + throw new Error("getCompletionEntrySymbol not implemented across the shim layer."); + } + getQuickInfoAtPosition(fileName: string, position: number): ts.QuickInfo { + return unwrapJSONCallResult(this.shim.getQuickInfoAtPosition(fileName, position)); + } + getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): ts.TextSpan { + return unwrapJSONCallResult(this.shim.getNameOrDottedNameSpan(fileName, startPos, endPos)); + } + getBreakpointStatementAtPosition(fileName: string, position: number): ts.TextSpan { + return unwrapJSONCallResult(this.shim.getBreakpointStatementAtPosition(fileName, position)); + } + getSignatureHelpItems(fileName: string, position: number, options: ts.SignatureHelpItemsOptions | undefined): ts.SignatureHelpItems { + return unwrapJSONCallResult(this.shim.getSignatureHelpItems(fileName, position, options)); + } + getRenameInfo(fileName: string, position: number, options?: ts.RenameInfoOptions): ts.RenameInfo { + return unwrapJSONCallResult(this.shim.getRenameInfo(fileName, position, options)); + } + getSmartSelectionRange(fileName: string, position: number): ts.SelectionRange { + return unwrapJSONCallResult(this.shim.getSmartSelectionRange(fileName, position)); + } + findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): ts.RenameLocation[] { + return unwrapJSONCallResult(this.shim.findRenameLocations(fileName, position, findInStrings, findInComments, providePrefixAndSuffixTextForRename)); + } + getDefinitionAtPosition(fileName: string, position: number): ts.DefinitionInfo[] { + return unwrapJSONCallResult(this.shim.getDefinitionAtPosition(fileName, position)); + } + getDefinitionAndBoundSpan(fileName: string, position: number): ts.DefinitionInfoAndBoundSpan { + return unwrapJSONCallResult(this.shim.getDefinitionAndBoundSpan(fileName, position)); + } + getTypeDefinitionAtPosition(fileName: string, position: number): ts.DefinitionInfo[] { + return unwrapJSONCallResult(this.shim.getTypeDefinitionAtPosition(fileName, position)); + } + getImplementationAtPosition(fileName: string, position: number): ts.ImplementationLocation[] { + return unwrapJSONCallResult(this.shim.getImplementationAtPosition(fileName, position)); + } + getReferencesAtPosition(fileName: string, position: number): ts.ReferenceEntry[] { + return unwrapJSONCallResult(this.shim.getReferencesAtPosition(fileName, position)); + } + findReferences(fileName: string, position: number): ts.ReferencedSymbol[] { + return unwrapJSONCallResult(this.shim.findReferences(fileName, position)); + } + getOccurrencesAtPosition(fileName: string, position: number): ts.ReferenceEntry[] { + return unwrapJSONCallResult(this.shim.getOccurrencesAtPosition(fileName, position)); + } + getDocumentHighlights(fileName: string, position: number, filesToSearch: string[]): ts.DocumentHighlights[] { + return unwrapJSONCallResult(this.shim.getDocumentHighlights(fileName, position, JSON.stringify(filesToSearch))); + } + getNavigateToItems(searchValue: string): ts.NavigateToItem[] { + return unwrapJSONCallResult(this.shim.getNavigateToItems(searchValue)); + } + getNavigationBarItems(fileName: string): ts.NavigationBarItem[] { + return unwrapJSONCallResult(this.shim.getNavigationBarItems(fileName)); + } + getNavigationTree(fileName: string): ts.NavigationTree { + return unwrapJSONCallResult(this.shim.getNavigationTree(fileName)); + } + getOutliningSpans(fileName: string): ts.OutliningSpan[] { + return unwrapJSONCallResult(this.shim.getOutliningSpans(fileName)); + } + getTodoComments(fileName: string, descriptors: ts.TodoCommentDescriptor[]): ts.TodoComment[] { + return unwrapJSONCallResult(this.shim.getTodoComments(fileName, JSON.stringify(descriptors))); + } + getBraceMatchingAtPosition(fileName: string, position: number): ts.TextSpan[] { + return unwrapJSONCallResult(this.shim.getBraceMatchingAtPosition(fileName, position)); + } + getIndentationAtPosition(fileName: string, position: number, options: ts.EditorOptions): number { + return unwrapJSONCallResult(this.shim.getIndentationAtPosition(fileName, position, JSON.stringify(options))); + } + getFormattingEditsForRange(fileName: string, start: number, end: number, options: ts.FormatCodeOptions): ts.TextChange[] { + return unwrapJSONCallResult(this.shim.getFormattingEditsForRange(fileName, start, end, JSON.stringify(options))); + } + getFormattingEditsForDocument(fileName: string, options: ts.FormatCodeOptions): ts.TextChange[] { + return unwrapJSONCallResult(this.shim.getFormattingEditsForDocument(fileName, JSON.stringify(options))); + } + getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: ts.FormatCodeOptions): ts.TextChange[] { + return unwrapJSONCallResult(this.shim.getFormattingEditsAfterKeystroke(fileName, position, key, JSON.stringify(options))); + } + getDocCommentTemplateAtPosition(fileName: string, position: number): ts.TextInsertion { + return unwrapJSONCallResult(this.shim.getDocCommentTemplateAtPosition(fileName, position)); + } + isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean { + return unwrapJSONCallResult(this.shim.isValidBraceCompletionAtPosition(fileName, position, openingBrace)); + } + getJsxClosingTagAtPosition(): never { + throw new Error("Not supported on the shim."); + } + getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): ts.TextSpan { + return unwrapJSONCallResult(this.shim.getSpanOfEnclosingComment(fileName, position, onlyMultiLine)); + } + getCodeFixesAtPosition(): never { + throw new Error("Not supported on the shim."); + } + getCombinedCodeFix = ts.notImplemented; + applyCodeActionCommand = ts.notImplemented; + getCodeFixDiagnostics(): ts.Diagnostic[] { + throw new Error("Not supported on the shim."); + } + getEditsForRefactor(): ts.RefactorEditInfo { + throw new Error("Not supported on the shim."); + } + getApplicableRefactors(): ts.ApplicableRefactorInfo[] { + throw new Error("Not supported on the shim."); + } + organizeImports(_scope: ts.OrganizeImportsScope, _formatOptions: ts.FormatCodeSettings): readonly ts.FileTextChanges[] { + throw new Error("Not supported on the shim."); + } + getEditsForFileRename(): readonly ts.FileTextChanges[] { + throw new Error("Not supported on the shim."); + } + prepareCallHierarchy(fileName: string, position: number) { + return unwrapJSONCallResult(this.shim.prepareCallHierarchy(fileName, position)); + } + provideCallHierarchyIncomingCalls(fileName: string, position: number) { + return unwrapJSONCallResult(this.shim.provideCallHierarchyIncomingCalls(fileName, position)); + } + provideCallHierarchyOutgoingCalls(fileName: string, position: number) { + return unwrapJSONCallResult(this.shim.provideCallHierarchyOutgoingCalls(fileName, position)); + } + getEmitOutput(fileName: string): ts.EmitOutput { + return unwrapJSONCallResult(this.shim.getEmitOutput(fileName)); + } + getProgram(): ts.Program { + throw new Error("Program can not be marshaled across the shim layer."); + } + getNonBoundSourceFile(): ts.SourceFile { + throw new Error("SourceFile can not be marshaled across the shim layer."); + } + getSourceFile(): ts.SourceFile { + throw new Error("SourceFile can not be marshaled across the shim layer."); + } + getSourceMapper(): never { + return ts.notImplemented(); + } + clearSourceMapperCache(): never { + return ts.notImplemented(); + } + toggleLineComment(fileName: string, textRange: ts.TextRange): ts.TextChange[] { + return unwrapJSONCallResult(this.shim.toggleLineComment(fileName, textRange)); + } + toggleMultilineComment(fileName: string, textRange: ts.TextRange): ts.TextChange[] { + return unwrapJSONCallResult(this.shim.toggleMultilineComment(fileName, textRange)); + } + commentSelection(fileName: string, textRange: ts.TextRange): ts.TextChange[] { + return unwrapJSONCallResult(this.shim.commentSelection(fileName, textRange)); + } + uncommentSelection(fileName: string, textRange: ts.TextRange): ts.TextChange[] { + return unwrapJSONCallResult(this.shim.uncommentSelection(fileName, textRange)); + } + dispose(): void { this.shim.dispose({}); } + } + + export class ShimLanguageServiceAdapter implements LanguageServiceAdapter { + private host: ShimLanguageServiceHost; + private factory: ts.TypeScriptServicesFactory; + constructor(preprocessToResolve: boolean, cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) { + this.host = new ShimLanguageServiceHost(preprocessToResolve, cancellationToken, options); + this.factory = new ts.TypeScriptServicesFactory(); + } + getHost() { return this.host; } + getLanguageService(): ts.LanguageService { return new LanguageServiceShimProxy(this.factory.createLanguageServiceShim(this.host)); } + getClassifier(): ts.Classifier { return new ClassifierShimProxy(this.factory.createClassifierShim(this.host)); } + getPreProcessedFileInfo(fileName: string, fileContents: string): ts.PreProcessedFileInfo { + const coreServicesShim = this.factory.createCoreServicesShim(this.host); + const shimResult: { + referencedFiles: ts.ShimsFileReference[]; + typeReferenceDirectives: ts.ShimsFileReference[]; + importedFiles: ts.ShimsFileReference[]; + isLibFile: boolean; + } = unwrapJSONCallResult(coreServicesShim.getPreProcessedFileInfo(fileName, ts.ScriptSnapshot.fromString(fileContents))); + + const convertResult: ts.PreProcessedFileInfo = { + referencedFiles: [], + importedFiles: [], + ambientExternalModules: [], + isLibFile: shimResult.isLibFile, + typeReferenceDirectives: [], + libReferenceDirectives: [] + }; + + ts.forEach(shimResult.referencedFiles, refFile => { + convertResult.referencedFiles.push({ + fileName: refFile.path, + pos: refFile.position, + end: refFile.position + refFile.length + }); + }); + + ts.forEach(shimResult.importedFiles, importedFile => { + convertResult.importedFiles.push({ + fileName: importedFile.path, + pos: importedFile.position, + end: importedFile.position + importedFile.length + }); + }); + + ts.forEach(shimResult.typeReferenceDirectives, typeRefDirective => { + convertResult.importedFiles.push({ + fileName: typeRefDirective.path, + pos: typeRefDirective.position, + end: typeRefDirective.position + typeRefDirective.length + }); + }); + return convertResult; + } + } + + // Server adapter + class SessionClientHost extends NativeLanguageServiceHost implements ts.server.SessionClientHost { + private client!: ts.server.SessionClient; + + constructor(cancellationToken: ts.HostCancellationToken | undefined, settings: ts.CompilerOptions | undefined) { + super(cancellationToken, settings); + } + + onMessage = ts.noop; + writeMessage = ts.noop; + + setClient(client: ts.server.SessionClient) { + this.client = client; + } + + openFile(fileName: string, content?: string, scriptKindName?: "TS" | "JS" | "TSX" | "JSX"): void { + super.openFile(fileName, content, scriptKindName); + this.client.openFile(fileName, content, scriptKindName); + } + + editScript(fileName: string, start: number, end: number, newText: string) { + const changeArgs = this.client.createChangeFileRequestArgs(fileName, start, end, newText); + super.editScript(fileName, start, end, newText); + this.client.changeFile(fileName, changeArgs); + } + } + + class SessionServerHost implements ts.server.ServerHost, ts.server.Logger { + args: string[] = []; + newLine: string; + useCaseSensitiveFileNames = false; + + constructor(private host: NativeLanguageServiceHost) { + this.newLine = this.host.getNewLine(); + } + + onMessage = ts.noop; + writeMessage = ts.noop; // overridden + write(message: string): void { + this.writeMessage(message); + } + + readFile(fileName: string): string | undefined { + if (ts.stringContains(fileName, Compiler.defaultLibFileName)) { + fileName = Compiler.defaultLibFileName; + } + + const snapshot = this.host.getScriptSnapshot(fileName); + return snapshot && ts.getSnapshotText(snapshot); + } + + writeFile = ts.noop; + + resolvePath(path: string): string { + return path; + } + + fileExists(path: string): boolean { + return !!this.host.getScriptSnapshot(path); + } + + directoryExists(): boolean { + // for tests assume that directory exists + return true; + } + + getExecutingFilePath(): string { + return ""; + } + + exit = ts.noop; + + createDirectory(_directoryName: string): void { + return ts.notImplemented(); + } + + getCurrentDirectory(): string { + return this.host.getCurrentDirectory(); + } + + getDirectories(path: string): string[] { + return this.host.getDirectories(path); + } + + getEnvironmentVariable(name: string): string { + return ts.sys.getEnvironmentVariable(name); + } + + readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[] { + return this.host.readDirectory(path, extensions, exclude, include, depth); + } + + watchFile(): ts.FileWatcher { + return { close: ts.noop }; + } + + watchDirectory(): ts.FileWatcher { + return { close: ts.noop }; + } + + close = ts.noop; + + info(message: string): void { + this.host.log(message); + } + + msg(message: string): void { + this.host.log(message); + } + + loggingEnabled() { + return true; + } + + getLogFileName(): string | undefined { + return undefined; + } + + hasLevel() { + return false; + } + + startGroup() { throw ts.notImplemented(); } + endGroup() { throw ts.notImplemented(); } + + perftrc(message: string): void { + return this.host.log(message); + } + + setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): any { + // eslint-disable-next-line no-restricted-globals + return setTimeout(callback, ms, args); + } + + clearTimeout(timeoutId: any): void { + // eslint-disable-next-line no-restricted-globals + clearTimeout(timeoutId); + } + + setImmediate(callback: (...args: any[]) => void, _ms: number, ...args: any[]): any { + // eslint-disable-next-line no-restricted-globals + return setImmediate(callback, args); + } + + clearImmediate(timeoutId: any): void { + // eslint-disable-next-line no-restricted-globals + clearImmediate(timeoutId); + } + + createHash(s: string) { + return mockHash(s); + } + + require(_initialDir: string, _moduleName: string): ts.RequireResult { + switch (_moduleName) { + // Adds to the Quick Info a fixed string and a string from the config file + // and replaces the first display part + case "quickinfo-augmeneter": + return { + module: () => ({ + create(info: ts.server.PluginCreateInfo) { + const proxy = makeDefaultProxy(info); + const langSvc: any = info.languageService; + // eslint-disable-next-line only-arrow-functions + proxy.getQuickInfoAtPosition = function () { + const parts = langSvc.getQuickInfoAtPosition.apply(langSvc, arguments); + if (parts.displayParts.length > 0) { + parts.displayParts[0].text = "Proxied"; + } + parts.displayParts.push({ text: info.config.message, kind: "punctuation" }); + return parts; + }; + + return proxy; + } + }), + error: undefined + }; + + // Throws during initialization + case "create-thrower": + return { + module: () => ({ + create() { + throw new Error("I am not a well-behaved plugin"); + } + }), + error: undefined + }; + + // Adds another diagnostic + case "diagnostic-adder": + return { + module: () => ({ + create(info: ts.server.PluginCreateInfo) { + const proxy = makeDefaultProxy(info); + proxy.getSemanticDiagnostics = filename => { + const prev = info.languageService.getSemanticDiagnostics(filename); + const sourceFile: ts.SourceFile = info.project.getSourceFile(ts.toPath(filename, /*basePath*/ undefined, ts.createGetCanonicalFileName(info.serverHost.useCaseSensitiveFileNames)))!; + prev.push({ + category: ts.DiagnosticCategory.Warning, + file: sourceFile, + code: 9999, + length: 3, + messageText: `Plugin diagnostic`, + start: 0 + }); + return prev; + }; + return proxy; + } + }), + error: undefined + }; + + // Accepts configurations + case "configurable-diagnostic-adder": + let customMessage = "default message"; + return { + module: () => ({ + create(info: ts.server.PluginCreateInfo) { + customMessage = info.config.message; + const proxy = makeDefaultProxy(info); + proxy.getSemanticDiagnostics = filename => { + const prev = info.languageService.getSemanticDiagnostics(filename); + const sourceFile: ts.SourceFile = info.project.getSourceFile(ts.toPath(filename, /*basePath*/ undefined, ts.createGetCanonicalFileName(info.serverHost.useCaseSensitiveFileNames)))!; + prev.push({ + category: ts.DiagnosticCategory.Error, + file: sourceFile, + code: 9999, + length: 3, + messageText: customMessage, + start: 0 + }); + return prev; + }; + return proxy; + }, + onConfigurationChanged(config: any) { + customMessage = config.message; + } + }), + error: undefined + }; + + default: + return { + module: undefined, + error: new Error("Could not resolve module") + }; + } + } + } + + class FourslashSession extends ts.server.Session { + getText(fileName: string) { + return ts.getSnapshotText(this.projectService.getDefaultProjectForFile(ts.server.toNormalizedPath(fileName), /*ensureProject*/ true)!.getScriptSnapshot(fileName)!); + } + } + + export class ServerLanguageServiceAdapter implements LanguageServiceAdapter { + private host: SessionClientHost; + private client: ts.server.SessionClient; + private server: FourslashSession; + constructor(cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) { + // This is the main host that tests use to direct tests + const clientHost = new SessionClientHost(cancellationToken, options); + const client = new ts.server.SessionClient(clientHost); + + // This host is just a proxy for the clientHost, it uses the client + // host to answer server queries about files on disk + const serverHost = new SessionServerHost(clientHost); + const opts: ts.server.SessionOptions = { + host: serverHost, + cancellationToken: ts.server.nullCancellationToken, + useSingleInferredProject: false, + useInferredProjectPerProjectRoot: false, + typingsInstaller: undefined!, // TODO: GH#18217 + byteLength: Utils.byteLength, + hrtime: process.hrtime, + logger: serverHost, + canUseEvents: true + }; + this.server = new FourslashSession(opts); + + + // Fake the connection between the client and the server + serverHost.writeMessage = client.onMessage.bind(client); + clientHost.writeMessage = this.server.onMessage.bind(this.server); + + // Wire the client to the host to get notifications when a file is open + // or edited. + clientHost.setClient(client); + + // Set the properties + this.client = client; + this.host = clientHost; + } + getHost() { return this.host; } + getLanguageService(): ts.LanguageService { return this.client; } + getClassifier(): ts.Classifier { throw new Error("getClassifier is not available using the server interface."); } + getPreProcessedFileInfo(): ts.PreProcessedFileInfo { throw new Error("getPreProcessedFileInfo is not available using the server interface."); } + assertTextConsistent(fileName: string) { + const serverText = this.server.getText(fileName); + const clientText = this.host.readFile(fileName); + ts.Debug.assert(serverText === clientText, [ + "Server and client text are inconsistent.", + "", + "\x1b[1mServer\x1b[0m\x1b[31m:", + serverText, + "", + "\x1b[1mClient\x1b[0m\x1b[31m:", + clientText, + "", + "This probably means something is wrong with the fourslash infrastructure, not with the test." + ].join(ts.sys.newLine)); + } + } +}