diff --git a/src/calls.ts b/src/calls.ts index 8bf22699..a348a0b5 100644 --- a/src/calls.ts +++ b/src/calls.ts @@ -1,11 +1,12 @@ import type tsp from 'typescript/lib/protocol.d.js'; import * as lsp from 'vscode-languageserver'; +import { TextDocument } from 'vscode-languageserver-textdocument'; import * as lspcalls from './lsp-protocol.calls.proposed.js'; import { TspClient } from './tsp-client.js'; import { CommandTypes } from './tsp-command-types.js'; -import { uriToPath, toLocation, asRange, Range, toSymbolKind, pathToUri } from './protocol-translation.js'; -import { TextDocument } from 'vscode-languageserver-textdocument'; +import { uriToPath, toLocation, toSymbolKind, pathToUri } from './protocol-translation.js'; +import { Range } from './utils/typeConverters.js'; export async function computeCallers(tspClient: TspClient, args: lsp.TextDocumentPositionParams): Promise { const nullResult = { calls: [] }; @@ -140,7 +141,7 @@ async function findEnclosingSymbol(tspClient: TspClient, args: tsp.FileSpan): Pr return undefined; } const pos = lsp.Position.create(args.start.line - 1, args.start.offset - 1); - const symbol = await findEnclosingSymbolInTree(tree, lsp.Range.create(pos, pos)); + const symbol = findEnclosingSymbolInTree(tree, lsp.Range.create(pos, pos)); if (!symbol) { return undefined; } @@ -149,7 +150,7 @@ async function findEnclosingSymbol(tspClient: TspClient, args: tsp.FileSpan): Pr } function findEnclosingSymbolInTree(parent: tsp.NavigationTree, range: lsp.Range): lsp.DocumentSymbol | undefined { - const inSpan = (span: tsp.TextSpan) => !!Range.intersection(asRange(span), range); + const inSpan = (span: tsp.TextSpan) => !!Range.intersection(Range.fromTextSpan(span), range); const inTree = (tree: tsp.NavigationTree) => tree.spans.some(span => inSpan(span)); let candidate = inTree(parent) ? parent : undefined; @@ -167,10 +168,10 @@ function findEnclosingSymbolInTree(parent: tsp.NavigationTree, range: lsp.Range) return undefined; } const span = candidate.spans.find(span => inSpan(span))!; - const spanRange = asRange(span); + const spanRange = Range.fromTextSpan(span); let selectionRange = spanRange; if (candidate.nameSpan) { - const nameRange = asRange(candidate.nameSpan); + const nameRange = Range.fromTextSpan(candidate.nameSpan); if (Range.intersection(spanRange, nameRange)) { selectionRange = nameRange; } diff --git a/src/completion.ts b/src/completion.ts index fa3e6da0..c31ea284 100644 --- a/src/completion.ts +++ b/src/completion.ts @@ -9,12 +9,12 @@ import * as lsp from 'vscode-languageserver'; import type tsp from 'typescript/lib/protocol.js'; import { LspDocument } from './document.js'; import { CommandTypes, KindModifiers, ScriptElementKind } from './tsp-command-types.js'; -import { asRange, toTextEdit, asPlainText, asDocumentation, normalizePath } from './protocol-translation.js'; +import { toTextEdit, asPlainText, asDocumentation, normalizePath } from './protocol-translation.js'; import { Commands } from './commands.js'; import { TspClient } from './tsp-client.js'; import { CompletionOptions, DisplayPartKind, SupportedFeatures } from './ts-protocol.js'; import SnippetString from './utils/SnippetString.js'; -import * as typeConverters from './utils/typeConverters.js'; +import { Range, Position } from './utils/typeConverters.js'; interface ParameterListParts { readonly parts: ReadonlyArray; @@ -62,7 +62,7 @@ export function asCompletionItem(entry: tsp.CompletionEntry, file: string, posit } let insertText = entry.insertText; - let replacementRange = entry.replacementSpan && asRange(entry.replacementSpan); + let replacementRange = entry.replacementSpan && Range.fromTextSpan(entry.replacementSpan); // Make sure we only replace a single line at most if (replacementRange && replacementRange.start.line !== replacementRange.end.line) { replacementRange = lsp.Range.create(replacementRange.start, document.getLineEnd(replacementRange.start.line)); @@ -202,7 +202,7 @@ export async function asResolvedCompletionItem( } if (features.completionSnippets && options.completeFunctionCalls && (item.kind === lsp.CompletionItemKind.Function || item.kind === lsp.CompletionItemKind.Method)) { const { line, offset } = item.data; - const position = typeConverters.Position.fromLocation({ line, offset }); + const position = Position.fromLocation({ line, offset }); const shouldCompleteFunction = await isValidFunctionCompletionContext(filepath, position, client); if (shouldCompleteFunction) { createSnippetOfFunctionCall(item, details); @@ -216,7 +216,7 @@ export async function isValidFunctionCompletionContext(filepath: string, positio // Workaround for https://github.com/Microsoft/TypeScript/issues/12677 // Don't complete function calls inside of destructive assigments or imports try { - const args: tsp.FileLocationRequestArgs = typeConverters.Position.toFileLocationRequestArgs(filepath, position); + const args: tsp.FileLocationRequestArgs = Position.toFileLocationRequestArgs(filepath, position); const response = await client.request(CommandTypes.Quickinfo, args); if (response.type !== 'response') { return true; diff --git a/src/document-symbol.ts b/src/document-symbol.ts index a40d20a5..00b4ccb6 100644 --- a/src/document-symbol.ts +++ b/src/document-symbol.ts @@ -7,18 +7,19 @@ import * as lsp from 'vscode-languageserver'; import type tsp from 'typescript/lib/protocol.d.js'; -import { asRange, toSymbolKind, Range } from './protocol-translation.js'; +import { toSymbolKind } from './protocol-translation.js'; import { ScriptElementKind } from './tsp-command-types.js'; +import { Range } from './utils/typeConverters.js'; export function collectDocumentSymbols(parent: tsp.NavigationTree, symbols: lsp.DocumentSymbol[]): boolean { - return collectDocumentSymbolsInRange(parent, symbols, { start: asRange(parent.spans[0]).start, end: asRange(parent.spans[parent.spans.length - 1]).end }); + return collectDocumentSymbolsInRange(parent, symbols, { start: Range.fromTextSpan(parent.spans[0]).start, end: Range.fromTextSpan(parent.spans[parent.spans.length - 1]).end }); } function collectDocumentSymbolsInRange(parent: tsp.NavigationTree, symbols: lsp.DocumentSymbol[], range: lsp.Range): boolean { let shouldInclude = shouldIncludeEntry(parent); for (const span of parent.spans) { - const spanRange = asRange(span); + const spanRange = Range.fromTextSpan(span); if (!Range.intersection(range, spanRange)) { continue; } @@ -26,7 +27,7 @@ function collectDocumentSymbolsInRange(parent: tsp.NavigationTree, symbols: lsp. const children: lsp.DocumentSymbol[] = []; if (parent.childItems) { for (const child of parent.childItems) { - if (child.spans.some(childSpan => !!Range.intersection(spanRange, asRange(childSpan)))) { + if (child.spans.some(childSpan => !!Range.intersection(spanRange, Range.fromTextSpan(childSpan)))) { const includedChild = collectDocumentSymbolsInRange(child, children, spanRange); shouldInclude = shouldInclude || includedChild; } @@ -34,7 +35,7 @@ function collectDocumentSymbolsInRange(parent: tsp.NavigationTree, symbols: lsp. } let selectionRange = spanRange; if (parent.nameSpan) { - const nameRange = asRange(parent.nameSpan); + const nameRange = Range.fromTextSpan(parent.nameSpan); // In the case of mergeable definitions, the nameSpan is only correct for the first definition. if (Range.intersection(spanRange, nameRange)) { selectionRange = nameRange; @@ -59,11 +60,11 @@ export function collectSymbolInformation(uri: string, current: tsp.NavigationTre let shouldInclude = shouldIncludeEntry(current); const name = current.text; for (const span of current.spans) { - const range = asRange(span); + const range = Range.fromTextSpan(span); const children: lsp.SymbolInformation[] = []; if (current.childItems) { for (const child of current.childItems) { - if (child.spans.some(span => !!Range.intersection(range, asRange(span)))) { + if (child.spans.some(span => !!Range.intersection(range, Range.fromTextSpan(span)))) { const includedChild = collectSymbolInformation(uri, child, children, name); shouldInclude = shouldInclude || includedChild; } diff --git a/src/features/fix-all.ts b/src/features/fix-all.ts index 39c407c4..ef3ebc42 100644 --- a/src/features/fix-all.ts +++ b/src/features/fix-all.ts @@ -6,12 +6,13 @@ import type tsp from 'typescript/lib/protocol.d.js'; import * as lsp from 'vscode-languageserver'; import { LspDocuments } from '../document.js'; -import { toFileRangeRequestArgs, toTextDocumentEdit } from '../protocol-translation.js'; +import { toTextDocumentEdit } from '../protocol-translation.js'; import { TspClient } from '../tsp-client.js'; import { CommandTypes } from '../tsp-command-types.js'; import * as errorCodes from '../utils/errorCodes.js'; import * as fixNames from '../utils/fixNames.js'; import { CodeActionKind } from '../utils/types.js'; +import { Range } from '../utils/typeConverters.js'; interface AutoFix { readonly codes: Set; @@ -33,7 +34,7 @@ async function buildIndividualFixes( } const args: tsp.CodeFixRequestArgs = { - ...toFileRangeRequestArgs(file, diagnostic.range), + ...Range.toFileRangeRequestArgs(file, diagnostic.range), errorCodes: [+diagnostic.code!] }; @@ -67,7 +68,7 @@ async function buildCombinedFix( } const args: tsp.CodeFixRequestArgs = { - ...toFileRangeRequestArgs(file, diagnostic.range), + ...Range.toFileRangeRequestArgs(file, diagnostic.range), errorCodes: [+diagnostic.code!] }; diff --git a/src/features/source-definition.ts b/src/features/source-definition.ts index a92652b1..1bd61d85 100644 --- a/src/features/source-definition.ts +++ b/src/features/source-definition.ts @@ -11,7 +11,7 @@ import * as lsp from 'vscode-languageserver'; import API from '../utils/api.js'; -import * as typeConverters from '../utils/typeConverters.js'; +import { Position } from '../utils/typeConverters.js'; import { toLocation, uriToPath } from '../protocol-translation.js'; import type { LspDocuments } from '../document.js'; import type { TspClient } from '../tsp-client.js'; @@ -54,7 +54,7 @@ export class SourceDefinitionCommand { return; } - const args = typeConverters.Position.toFileLocationRequestArgs(file, position); + const args = Position.toFileLocationRequestArgs(file, position); return await lspClient.withProgress({ message: 'Finding source definitions…', reporter diff --git a/src/lsp-server.spec.ts b/src/lsp-server.spec.ts index 4a99177a..707bd4f3 100644 --- a/src/lsp-server.spec.ts +++ b/src/lsp-server.spec.ts @@ -406,6 +406,121 @@ describe('completion', () => { }); }); +describe('definition', () => { + it('goes to definition', async () => { + // NOTE: This test needs to reference files that physically exist for the feature to work. + const indexUri = uri('source-definition', 'index.ts'); + const indexDoc = { + uri: indexUri, + languageId: 'typescript', + version: 1, + text: readContents(filePath('source-definition', 'index.ts')) + }; + server.didOpenTextDocument({ textDocument: indexDoc }); + const definitions = await server.definition({ + textDocument: indexDoc, + position: position(indexDoc, 'a/*identifier*/') + }) as lsp.Location[]; + assert.isArray(definitions); + assert.equal(definitions!.length, 1); + assert.deepEqual(definitions![0], { + uri: uri('source-definition', 'a.d.ts'), + range: { + start: { + line: 0, + character: 21 + }, + end: { + line: 0, + character: 22 + } + } + }); + }); +}); + +describe('definition (definition link supported)', () => { + let localServer: TestLspServer; + + before(async () => { + const clientCapabilitiesOverride: lsp.ClientCapabilities = { + textDocument: { + definition: { + linkSupport: true + } + } + }; + localServer = await createServer({ + rootUri: uri('source-definition'), + publishDiagnostics: args => diagnostics.set(args.uri, args), + clientCapabilitiesOverride + }); + }); + + beforeEach(() => { + localServer.closeAll(); + // "closeAll" triggers final publishDiagnostics with an empty list so clear last. + diagnostics.clear(); + localServer.workspaceEdits = []; + }); + + after(() => { + localServer.closeAll(); + localServer.shutdown(); + }); + + it('goes to definition', async () => { + // NOTE: This test needs to reference files that physically exist for the feature to work. + const indexUri = uri('source-definition', 'index.ts'); + const indexDoc = { + uri: indexUri, + languageId: 'typescript', + version: 1, + text: readContents(filePath('source-definition', 'index.ts')) + }; + localServer.didOpenTextDocument({ textDocument: indexDoc }); + const definitions = await localServer.definition({ + textDocument: indexDoc, + position: position(indexDoc, 'a/*identifier*/') + }) as lsp.DefinitionLink[]; + assert.isArray(definitions); + assert.equal(definitions!.length, 1); + assert.deepEqual(definitions![0], { + originSelectionRange: { + start: { + line: 1, + character: 0 + }, + end: { + line: 1, + character: 1 + } + }, + targetRange: { + start: { + line: 0, + character: 0 + }, + end: { + line: 0, + character: 30 + } + }, + targetUri: uri('source-definition', 'a.d.ts'), + targetSelectionRange: { + start: { + line: 0, + character: 21 + }, + end: { + line: 0, + character: 22 + } + } + }); + }); +}); + describe('diagnostics', () => { it('simple test', async () => { const doc = { diff --git a/src/lsp-server.ts b/src/lsp-server.ts index 7f3572a1..a0822d15 100644 --- a/src/lsp-server.ts +++ b/src/lsp-server.ts @@ -19,11 +19,7 @@ import { CommandTypes, EventTypes } from './tsp-command-types.js'; import { Logger, PrefixingLogger } from './logger.js'; import { TspClient } from './tsp-client.js'; import { DiagnosticEventQueue } from './diagnostic-queue.js'; -import { - toDocumentHighlight, asRange, asTagsDocumentation, - uriToPath, toSymbolKind, toLocation, toPosition, - pathToUri, toTextEdit, toFileRangeRequestArgs, asPlainText, normalizePath -} from './protocol-translation.js'; +import { toDocumentHighlight, asTagsDocumentation, uriToPath, toSymbolKind, toLocation, pathToUri, toTextEdit, asPlainText, normalizePath } from './protocol-translation.js'; import { LspDocuments, LspDocument } from './document.js'; import { asCompletionItem, asResolvedCompletionItem, getCompletionTriggerCharacter } from './completion.js'; import { asSignatureHelp, toTsTriggerReason } from './hover.js'; @@ -39,6 +35,7 @@ import { TypeScriptVersion, TypeScriptVersionProvider } from './utils/versionPro import { TypeScriptAutoFixProvider } from './features/fix-all.js'; import { SourceDefinitionCommand } from './features/source-definition.js'; import { LspClient } from './lsp-client.js'; +import { Position, Range } from './utils/typeConverters.js'; import { CodeActionKind } from './utils/types.js'; const DEFAULT_TSSERVER_PREFERENCES: Required = { @@ -221,6 +218,7 @@ export class LspServer { ...userInitializationOptions.preferences }; + // Setup supported features. const { textDocument } = clientCapabilities; const completionCapabilities = textDocument?.completion; if (completionCapabilities?.completionItem) { @@ -235,6 +233,7 @@ export class LspServer { this.features.diagnosticsTagSupport = true; } } + this.features.definitionLinkSupport = textDocument?.definition?.linkSupport && typescriptVersion.version?.gte(API.v270); const finalPreferences: TypeScriptInitializationOptions['preferences'] = { ...userPreferences, @@ -570,44 +569,78 @@ export class LspServer { // do nothing } - async definition(params: lsp.TextDocumentPositionParams): Promise { - // TODO: implement version checking and if semver.gte(version, 270) use `definitionAndBoundSpan` instead + async definition(params: lsp.DefinitionParams): Promise { return this.getDefinition({ - type: 'definition', + type: this.features.definitionLinkSupport ? CommandTypes.DefinitionAndBoundSpan : CommandTypes.Definition, params }); } - async implementation(params: lsp.TextDocumentPositionParams): Promise { - return this.getDefinition({ - type: 'implementation', + async implementation(params: lsp.TextDocumentPositionParams): Promise { + return this.getSymbolLocations({ + type: CommandTypes.Implementation, params }); } - async typeDefinition(params: lsp.TextDocumentPositionParams): Promise { - return this.getDefinition({ - type: 'typeDefinition', + async typeDefinition(params: lsp.TextDocumentPositionParams): Promise { + return this.getSymbolLocations({ + type: CommandTypes.TypeDefinition, params }); } - protected async getDefinition({ type, params }: { - type: 'definition' | 'implementation' | 'typeDefinition'; + private async getDefinition({ type, params }: { + type: CommandTypes.Definition | CommandTypes.DefinitionAndBoundSpan; + params: lsp.TextDocumentPositionParams; + }): Promise { + const file = uriToPath(params.textDocument.uri); + this.logger.log(type, params, file); + if (!file) { + return undefined; + } + + if (type === CommandTypes.DefinitionAndBoundSpan) { + const args = Position.toFileLocationRequestArgs(file, params.position); + const response = await this.tspClient.request(type, args); + if (response.type !== 'response' || !response.body) { + return undefined; + } + const span = Range.fromTextSpan(response.body.textSpan); + return response.body.definitions + .map((location): lsp.DefinitionLink => { + const target = toLocation(location, this.documents); + const targetRange = location.contextStart && location.contextEnd + ? Range.fromLocations(location.contextStart, location.contextEnd) + : target.range; + return { + originSelectionRange: span, + targetRange, + targetUri: target.uri, + targetSelectionRange: target.range + }; + }); + } + + return this.getSymbolLocations({ type: CommandTypes.Definition, params }); + } + + private async getSymbolLocations({ type, params }: { + type: CommandTypes.Definition | CommandTypes.Implementation | CommandTypes.TypeDefinition; params: lsp.TextDocumentPositionParams; - }): Promise { + }): Promise { const file = uriToPath(params.textDocument.uri); this.logger.log(type, params, file); if (!file) { return []; } - const result = await this.tspClient.request(type as CommandTypes.Definition, { - file, - line: params.position.line + 1, - offset: params.position.character + 1 - }); - return result.body ? result.body.map(fileSpan => toLocation(fileSpan, this.documents)) : []; + const args = Position.toFileLocationRequestArgs(file, params.position); + const response = await this.tspClient.request(type, args); + if (response.type !== 'response' || !response.body) { + return undefined; + } + return response.body.map(fileSpan => toLocation(fileSpan, this.documents)); } async documentSymbol(params: lsp.DocumentSymbolParams): Promise { @@ -714,7 +747,7 @@ export class LspServer { if (!result || !result.body) { return { contents: [] }; } - const range = asRange(result.body); + const range = Range.fromTextSpan(result.body); const contents: lsp.MarkedString[] = []; if (result.body.displayString) { contents.push({ language: 'typescript', value: result.body.displayString }); @@ -768,8 +801,8 @@ export class LspServer { textEdits.push({ newText: `${textSpan.prefixText || ''}${params.newName}${textSpan.suffixText || ''}`, range: { - start: toPosition(textSpan.start), - end: toPosition(textSpan.end) + start: Position.fromLocation(textSpan.start), + end: Position.fromLocation(textSpan.end) } }); }); @@ -914,7 +947,7 @@ export class LspServer { if (!file) { return []; } - const args = toFileRangeRequestArgs(file, params.range); + const args = Range.toFileRangeRequestArgs(file, params.range); const actions: lsp.CodeAction[] = []; const kinds = params.context.only?.map(kind => new CodeActionKind(kind)); if (!kinds || kinds.some(kind => kind.contains(CodeActionKind.QuickFix))) { @@ -1013,7 +1046,7 @@ export class LspServer { textDocument: { uri: pathToUri(args.file, this.documents) }, - position: toPosition(renameLocation) + position: Position.fromLocation(renameLocation) }); } } else if (arg.command === Commands.ORGANIZE_IMPORTS && arg.arguments) { @@ -1141,8 +1174,8 @@ export class LspServer { location: { uri: pathToUri(item.file, this.documents), range: { - start: toPosition(item.start), - end: toPosition(item.end) + start: Position.fromLocation(item.start), + end: Position.fromLocation(item.end) } }, kind: toSymbolKind(item.kind), @@ -1179,7 +1212,7 @@ export class LspServer { return foldingRanges; } protected asFoldingRange(span: tsp.OutliningSpan, document: LspDocument): lsp.FoldingRange | undefined { - const range = asRange(span.textSpan); + const range = Range.fromTextSpan(span.textSpan); const kind = this.asFoldingRangeKind(span); // workaround for https://github.com/Microsoft/vscode/issues/49904 @@ -1285,7 +1318,7 @@ export class LspServer { inlayHints: result.body?.map((item) => ({ text: item.text, - position: toPosition(item.position), + position: Position.fromLocation(item.position), whitespaceAfter: item.whitespaceAfter, whitespaceBefore: item.whitespaceBefore, kind: item.kind diff --git a/src/protocol-translation.ts b/src/protocol-translation.ts index 1062a58f..c427a9c2 100644 --- a/src/protocol-translation.ts +++ b/src/protocol-translation.ts @@ -10,6 +10,7 @@ import type tsp from 'typescript/lib/protocol.d.js'; import vscodeUri from 'vscode-uri'; import { LspDocuments } from './document.js'; import { SupportedFeatures } from './ts-protocol.js'; +import { Position } from './utils/typeConverters.js'; const RE_PATHSEP_WINDOWS = /\\/g; @@ -65,35 +66,16 @@ function currentVersion(filepath: string, documents: LspDocuments | undefined): return document ? document.version : null; } -export function toPosition(location: tsp.Location): lsp.Position { - // Clamping on the low side to 0 since Typescript returns 0, 0 when creating new file - // even though position is suppoed to be 1-based. - return { - line: Math.max(0, location.line - 1), - character: Math.max(0, location.offset - 1) - }; -} - export function toLocation(fileSpan: tsp.FileSpan, documents: LspDocuments | undefined): lsp.Location { return { uri: pathToUri(fileSpan.file, documents), range: { - start: toPosition(fileSpan.start), - end: toPosition(fileSpan.end) + start: Position.fromLocation(fileSpan.start), + end: Position.fromLocation(fileSpan.end) } }; } -export function toFileRangeRequestArgs(file: string, range: lsp.Range): tsp.FileRangeRequestArgs { - return { - file, - startLine: range.start.line + 1, - startOffset: range.start.character + 1, - endLine: range.end.line + 1, - endOffset: range.end.character + 1 - }; -} - const symbolKindsMapping: { [name: string]: lsp.SymbolKind; } = { 'enum member': lsp.SymbolKind.Constant, 'JSX attribute': lsp.SymbolKind.Property, @@ -136,8 +118,8 @@ function toDiagnosticSeverity(category: string): lsp.DiagnosticSeverity { export function toDiagnostic(diagnostic: tsp.Diagnostic, documents: LspDocuments | undefined, features: SupportedFeatures): lsp.Diagnostic { const lspDiagnostic: lsp.Diagnostic = { range: { - start: toPosition(diagnostic.start), - end: toPosition(diagnostic.end) + start: Position.fromLocation(diagnostic.start), + end: Position.fromLocation(diagnostic.end) }, message: diagnostic.text, severity: toDiagnosticSeverity(diagnostic.category), @@ -182,8 +164,8 @@ function asRelatedInformation(info: tsp.DiagnosticRelatedInformation[] | undefin export function toTextEdit(edit: tsp.CodeEdit): lsp.TextEdit { return { range: { - start: toPosition(edit.start), - end: toPosition(edit.end) + start: Position.fromLocation(edit.start), + end: Position.fromLocation(edit.end) }, newText: edit.newText }; @@ -204,8 +186,8 @@ export function toDocumentHighlight(item: tsp.DocumentHighlightsItem): lsp.Docum return { kind: toDocumentHighlightKind(i.kind), range: { - start: toPosition(i.start), - end: toPosition(i.end) + start: Position.fromLocation(i.start), + end: Position.fromLocation(i.end) } }; }); @@ -228,13 +210,6 @@ function toDocumentHighlightKind(kind: tsp.HighlightSpanKind): lsp.DocumentHighl } } -export function asRange(span: tsp.TextSpan): lsp.Range { - return lsp.Range.create( - Math.max(0, span.start.line - 1), Math.max(0, span.start.offset - 1), - Math.max(0, span.end.line - 1), Math.max(0, span.end.offset - 1) - ); -} - export function asDocumentation(data: { documentation?: tsp.SymbolDisplayPart[]; tags?: tsp.JSDocTagInfo[]; @@ -315,69 +290,3 @@ export function asPlainText(parts: string | tsp.SymbolDisplayPart[]): string { } return parts.map(part => part.text).join(''); } - -namespace Position { - export function Min(): undefined; - export function Min(...positions: lsp.Position[]): lsp.Position; - export function Min(...positions: lsp.Position[]): lsp.Position | undefined { - if (!positions.length) { - return undefined; - } - let result = positions.pop()!; - for (const p of positions) { - if (isBefore(p, result)) { - result = p; - } - } - return result; - } - export function isBefore(one: lsp.Position, other: lsp.Position): boolean { - if (one.line < other.line) { - return true; - } - if (other.line < one.line) { - return false; - } - return one.character < other.character; - } - export function Max(): undefined; - export function Max(...positions: lsp.Position[]): lsp.Position; - export function Max(...positions: lsp.Position[]): lsp.Position | undefined { - if (!positions.length) { - return undefined; - } - let result = positions.pop()!; - for (const p of positions) { - if (isAfter(p, result)) { - result = p; - } - } - return result; - } - export function isAfter(one: lsp.Position, other: lsp.Position): boolean { - return !isBeforeOrEqual(one, other); - } - export function isBeforeOrEqual(one: lsp.Position, other: lsp.Position): boolean { - if (one.line < other.line) { - return true; - } - if (other.line < one.line) { - return false; - } - return one.character <= other.character; - } -} - -export namespace Range { - export function intersection(one: lsp.Range, other: lsp.Range): lsp.Range | undefined { - const start = Position.Max(other.start, one.start); - const end = Position.Min(other.end, one.end); - if (Position.isAfter(start, end)) { - // this happens when there is no overlap: - // |-----| - // |----| - return undefined; - } - return lsp.Range.create(start, end); - } -} diff --git a/src/ts-protocol.ts b/src/ts-protocol.ts index e9f9a7d1..d79d13ff 100644 --- a/src/ts-protocol.ts +++ b/src/ts-protocol.ts @@ -29,6 +29,7 @@ export interface SupportedFeatures { completionLabelDetails?: boolean; completionSnippets?: boolean; diagnosticsTagSupport?: boolean; + definitionLinkSupport?: boolean; } export interface TypeScriptPlugin { diff --git a/src/utils/typeConverters.ts b/src/utils/typeConverters.ts index 6fa5e84a..3af9c0fc 100644 --- a/src/utils/typeConverters.ts +++ b/src/utils/typeConverters.ts @@ -5,14 +5,58 @@ /** * Helpers for converting FROM LanguageServer types language-server ts types */ -import type * as lsp from 'vscode-languageserver-protocol'; +import * as lsp from 'vscode-languageserver-protocol'; import type tsp from 'typescript/lib/protocol.d.js'; +export namespace Range { + export const fromTextSpan = (span: tsp.TextSpan): lsp.Range => fromLocations(span.start, span.end); + + export const toTextSpan = (range: lsp.Range): tsp.TextSpan => ({ + start: Position.toLocation(range.start), + end: Position.toLocation(range.end) + }); + + export const fromLocations = (start: tsp.Location, end: tsp.Location): lsp.Range => + lsp.Range.create( + Math.max(0, start.line - 1), Math.max(start.offset - 1, 0), + Math.max(0, end.line - 1), Math.max(0, end.offset - 1)); + + export const toFileRangeRequestArgs = (file: string, range: lsp.Range): tsp.FileRangeRequestArgs => ({ + file, + startLine: range.start.line + 1, + startOffset: range.start.character + 1, + endLine: range.end.line + 1, + endOffset: range.end.character + 1 + }); + + export const toFormattingRequestArgs = (file: string, range: lsp.Range): tsp.FormatRequestArgs => ({ + file, + line: range.start.line + 1, + offset: range.start.character + 1, + endLine: range.end.line + 1, + endOffset: range.end.character + 1 + }); + + export function intersection(one: lsp.Range, other: lsp.Range): lsp.Range | undefined { + const start = Position.Max(other.start, one.start); + const end = Position.Min(other.end, one.end); + if (Position.isAfter(start, end)) { + // this happens when there is no overlap: + // |-----| + // |----| + return undefined; + } + return lsp.Range.create(start, end); + } +} + export namespace Position { export const fromLocation = (tslocation: tsp.Location): lsp.Position => { + // Clamping on the low side to 0 since Typescript returns 0, 0 when creating new file + // even though position is supposed to be 1-based. return { - line: tslocation.line - 1, - character: tslocation.offset - 1 + line: Math.max(tslocation.line - 1, 0), + character: Math.max(tslocation.offset - 1, 0) }; }; @@ -26,4 +70,59 @@ export namespace Position { line: position.line + 1, offset: position.character + 1 }); + + export function Min(): undefined; + export function Min(...positions: lsp.Position[]): lsp.Position; + export function Min(...positions: lsp.Position[]): lsp.Position | undefined { + if (!positions.length) { + return undefined; + } + let result = positions.pop()!; + for (const p of positions) { + if (isBefore(p, result)) { + result = p; + } + } + return result; + } + export function isBefore(one: lsp.Position, other: lsp.Position): boolean { + if (one.line < other.line) { + return true; + } + if (other.line < one.line) { + return false; + } + return one.character < other.character; + } + export function Max(): undefined; + export function Max(...positions: lsp.Position[]): lsp.Position; + export function Max(...positions: lsp.Position[]): lsp.Position | undefined { + if (!positions.length) { + return undefined; + } + let result = positions.pop()!; + for (const p of positions) { + if (isAfter(p, result)) { + result = p; + } + } + return result; + } + export function isAfter(one: lsp.Position, other: lsp.Position): boolean { + return !isBeforeOrEqual(one, other); + } + export function isBeforeOrEqual(one: lsp.Position, other: lsp.Position): boolean { + if (one.line < other.line) { + return true; + } + if (other.line < one.line) { + return false; + } + return one.character <= other.character; + } +} + +export namespace Location { + export const fromTextSpan = (resource: lsp.DocumentUri, tsTextSpan: tsp.TextSpan): lsp.Location => + lsp.Location.create(resource, Range.fromTextSpan(tsTextSpan)); }