326 lines
14 KiB
Plaintext
326 lines
14 KiB
Plaintext
|
"use strict";
|
||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||
|
exports.handleKindModifiers = exports.register = void 0;
|
||
|
const semver = require("semver");
|
||
|
const getUserPreferences_1 = require("../../configs/getUserPreferences");
|
||
|
const PConst = require("../../protocol.const");
|
||
|
const shared_1 = require("../../shared");
|
||
|
const modifiers_1 = require("../../utils/modifiers");
|
||
|
function register(ctx) {
|
||
|
const { ts } = ctx;
|
||
|
const lt_320 = semver.lt(ts.version, '3.2.0');
|
||
|
const gte_300 = semver.gte(ts.version, '3.0.0');
|
||
|
return async (uri, position, options) => {
|
||
|
const document = ctx.getTextDocument(uri);
|
||
|
if (!document)
|
||
|
return;
|
||
|
const preferences = await (0, getUserPreferences_1.getUserPreferences)(ctx, document);
|
||
|
const fileName = ctx.uriToFileName(document.uri);
|
||
|
const offset = document.offsetAt(position);
|
||
|
const completionContext = (0, shared_1.safeCall)(() => ctx.languageService.getCompletionsAtPosition(fileName, offset, {
|
||
|
...preferences,
|
||
|
...options,
|
||
|
}));
|
||
|
if (completionContext === undefined)
|
||
|
return;
|
||
|
const wordRange = completionContext.optionalReplacementSpan ? {
|
||
|
start: document.positionAt(completionContext.optionalReplacementSpan.start),
|
||
|
end: document.positionAt(completionContext.optionalReplacementSpan.start + completionContext.optionalReplacementSpan.length),
|
||
|
} : undefined;
|
||
|
let line = document.getText({
|
||
|
start: { line: position.line, character: 0 },
|
||
|
end: { line: position.line + 1, character: 0 },
|
||
|
});
|
||
|
if (line.endsWith('\n')) {
|
||
|
line = line.substring(0, line.length - 1);
|
||
|
}
|
||
|
const dotAccessorContext = getDotAccessorContext(document);
|
||
|
const entries = completionContext.entries
|
||
|
.map(tsEntry => toVScodeItem(tsEntry, document));
|
||
|
return {
|
||
|
isIncomplete: !!completionContext.isIncomplete,
|
||
|
items: entries,
|
||
|
};
|
||
|
function toVScodeItem(tsEntry, document) {
|
||
|
const item = { label: tsEntry.name };
|
||
|
item.kind = convertKind(tsEntry.kind);
|
||
|
if (tsEntry.source && tsEntry.hasAction) {
|
||
|
// De-prioritize auto-imports
|
||
|
// https://github.com/microsoft/vscode/issues/40311
|
||
|
item.sortText = '\uffff' + tsEntry.sortText;
|
||
|
}
|
||
|
else {
|
||
|
item.sortText = tsEntry.sortText;
|
||
|
}
|
||
|
const { sourceDisplay, isSnippet, labelDetails } = tsEntry;
|
||
|
if (sourceDisplay) {
|
||
|
item.labelDetails ??= {};
|
||
|
item.labelDetails.description = ts.displayPartsToString(sourceDisplay);
|
||
|
}
|
||
|
if (labelDetails) {
|
||
|
item.labelDetails ??= {};
|
||
|
Object.assign(item.labelDetails, labelDetails);
|
||
|
}
|
||
|
item.preselect = tsEntry.isRecommended;
|
||
|
let range = getRangeFromReplacementSpan(tsEntry, document);
|
||
|
item.commitCharacters = getCommitCharacters(tsEntry, {
|
||
|
isNewIdentifierLocation: completionContext.isNewIdentifierLocation,
|
||
|
isInValidCommitCharacterContext: isInValidCommitCharacterContext(document, position),
|
||
|
enableCallCompletions: true, // TODO: suggest.completeFunctionCalls
|
||
|
});
|
||
|
item.insertText = tsEntry.insertText;
|
||
|
item.insertTextFormat = isSnippet ? 2 : 1;
|
||
|
item.filterText = getFilterText(tsEntry, wordRange, line, tsEntry.insertText);
|
||
|
if (completionContext?.isMemberCompletion && dotAccessorContext && !isSnippet) {
|
||
|
item.filterText = dotAccessorContext.text + (item.insertText || item.label);
|
||
|
if (!range) {
|
||
|
const replacementRange = wordRange;
|
||
|
if (replacementRange) {
|
||
|
range = {
|
||
|
inserting: dotAccessorContext.range,
|
||
|
replacing: rangeUnion(dotAccessorContext.range, replacementRange),
|
||
|
};
|
||
|
}
|
||
|
else {
|
||
|
range = dotAccessorContext.range;
|
||
|
}
|
||
|
item.insertText = item.filterText;
|
||
|
}
|
||
|
}
|
||
|
handleKindModifiers(item, tsEntry);
|
||
|
if (!range && wordRange) {
|
||
|
range = {
|
||
|
inserting: { start: wordRange.start, end: position },
|
||
|
replacing: wordRange,
|
||
|
};
|
||
|
}
|
||
|
if (range) {
|
||
|
if ('start' in range) {
|
||
|
item.textEdit = {
|
||
|
range,
|
||
|
newText: item.insertText || item.label,
|
||
|
};
|
||
|
}
|
||
|
else {
|
||
|
item.textEdit = {
|
||
|
insert: range.inserting,
|
||
|
replace: range.replacing,
|
||
|
newText: item.insertText || item.label,
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
return {
|
||
|
...item,
|
||
|
data: {
|
||
|
uri,
|
||
|
fileName,
|
||
|
offset,
|
||
|
originalItem: {
|
||
|
name: tsEntry.name,
|
||
|
source: tsEntry.source,
|
||
|
data: tsEntry.data,
|
||
|
labelDetails: tsEntry.labelDetails,
|
||
|
},
|
||
|
},
|
||
|
};
|
||
|
}
|
||
|
function getDotAccessorContext(document) {
|
||
|
let dotAccessorContext;
|
||
|
if (gte_300) {
|
||
|
if (!completionContext)
|
||
|
return;
|
||
|
const isMemberCompletion = completionContext.isMemberCompletion;
|
||
|
if (isMemberCompletion) {
|
||
|
const dotMatch = line.slice(0, position.character).match(/\??\.\s*$/) || undefined;
|
||
|
if (dotMatch) {
|
||
|
const range = {
|
||
|
start: { line: position.line, character: position.character - dotMatch[0].length },
|
||
|
end: position,
|
||
|
};
|
||
|
const text = document.getText(range);
|
||
|
dotAccessorContext = { range, text };
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return dotAccessorContext;
|
||
|
}
|
||
|
// from vscode typescript
|
||
|
function getRangeFromReplacementSpan(tsEntry, document) {
|
||
|
if (!tsEntry.replacementSpan) {
|
||
|
return;
|
||
|
}
|
||
|
let replaceRange = {
|
||
|
start: document.positionAt(tsEntry.replacementSpan.start),
|
||
|
end: document.positionAt(tsEntry.replacementSpan.start + tsEntry.replacementSpan.length),
|
||
|
};
|
||
|
// Make sure we only replace a single line at most
|
||
|
if (replaceRange.start.line !== replaceRange.end.line) {
|
||
|
replaceRange = {
|
||
|
start: {
|
||
|
line: replaceRange.start.line,
|
||
|
character: replaceRange.start.character,
|
||
|
},
|
||
|
end: {
|
||
|
line: replaceRange.start.line,
|
||
|
character: document.positionAt(document.offsetAt({ line: replaceRange.start.line + 1, character: 0 }) - 1).character,
|
||
|
},
|
||
|
};
|
||
|
}
|
||
|
// If TS returns an explicit replacement range, we should use it for both types of completion
|
||
|
return {
|
||
|
inserting: replaceRange,
|
||
|
replacing: replaceRange,
|
||
|
};
|
||
|
}
|
||
|
function getFilterText(tsEntry, wordRange, line, insertText) {
|
||
|
// Handle private field completions
|
||
|
if (tsEntry.name.startsWith('#')) {
|
||
|
const wordStart = wordRange ? line.charAt(wordRange.start.character) : undefined;
|
||
|
if (insertText) {
|
||
|
if (insertText.startsWith('this.#')) {
|
||
|
return wordStart === '#' ? insertText : insertText.replace(/^this\.#/, '');
|
||
|
}
|
||
|
else {
|
||
|
return insertText;
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
return wordStart === '#' ? undefined : tsEntry.name.replace(/^#/, '');
|
||
|
}
|
||
|
}
|
||
|
// For `this.` completions, generally don't set the filter text since we don't want them to be overly prioritized. #74164
|
||
|
if (insertText?.startsWith('this.')) {
|
||
|
return undefined;
|
||
|
}
|
||
|
// Handle the case:
|
||
|
// ```
|
||
|
// const xyz = { 'ab c': 1 };
|
||
|
// xyz.ab|
|
||
|
// ```
|
||
|
// In which case we want to insert a bracket accessor but should use `.abc` as the filter text instead of
|
||
|
// the bracketed insert text.
|
||
|
else if (insertText?.startsWith('[')) {
|
||
|
return insertText.replace(/^\[['"](.+)[['"]\]$/, '.$1');
|
||
|
}
|
||
|
// In all other cases, fallback to using the insertText
|
||
|
return insertText;
|
||
|
}
|
||
|
function convertKind(kind) {
|
||
|
switch (kind) {
|
||
|
case PConst.Kind.primitiveType:
|
||
|
case PConst.Kind.keyword:
|
||
|
return 14;
|
||
|
case PConst.Kind.const:
|
||
|
case PConst.Kind.let:
|
||
|
case PConst.Kind.variable:
|
||
|
case PConst.Kind.localVariable:
|
||
|
case PConst.Kind.alias:
|
||
|
case PConst.Kind.parameter:
|
||
|
return 6;
|
||
|
case PConst.Kind.memberVariable:
|
||
|
case PConst.Kind.memberGetAccessor:
|
||
|
case PConst.Kind.memberSetAccessor:
|
||
|
return 5;
|
||
|
case PConst.Kind.function:
|
||
|
case PConst.Kind.localFunction:
|
||
|
return 3;
|
||
|
case PConst.Kind.method:
|
||
|
case PConst.Kind.constructSignature:
|
||
|
case PConst.Kind.callSignature:
|
||
|
case PConst.Kind.indexSignature:
|
||
|
return 2;
|
||
|
case PConst.Kind.enum:
|
||
|
return 13;
|
||
|
case PConst.Kind.enumMember:
|
||
|
return 20;
|
||
|
case PConst.Kind.module:
|
||
|
case PConst.Kind.externalModuleName:
|
||
|
return 9;
|
||
|
case PConst.Kind.class:
|
||
|
case PConst.Kind.type:
|
||
|
return 7;
|
||
|
case PConst.Kind.interface:
|
||
|
return 8;
|
||
|
case PConst.Kind.warning:
|
||
|
return 1;
|
||
|
case PConst.Kind.script:
|
||
|
return 17;
|
||
|
case PConst.Kind.directory:
|
||
|
return 19;
|
||
|
case PConst.Kind.string:
|
||
|
return 21;
|
||
|
default:
|
||
|
return 10;
|
||
|
}
|
||
|
}
|
||
|
function getCommitCharacters(entry, context) {
|
||
|
if (entry.kind === PConst.Kind.warning) { // Ambient JS word based suggestion
|
||
|
return undefined;
|
||
|
}
|
||
|
if (context.isNewIdentifierLocation || !context.isInValidCommitCharacterContext) {
|
||
|
return undefined;
|
||
|
}
|
||
|
const commitCharacters = ['.', ',', ';'];
|
||
|
if (context.enableCallCompletions) {
|
||
|
commitCharacters.push('(');
|
||
|
}
|
||
|
return commitCharacters;
|
||
|
}
|
||
|
function isInValidCommitCharacterContext(document, position) {
|
||
|
if (lt_320) {
|
||
|
// Workaround for https://github.com/microsoft/TypeScript/issues/27742
|
||
|
// Only enable dot completions when the previous character is not a dot preceded by whitespace.
|
||
|
// Prevents incorrectly completing while typing spread operators.
|
||
|
if (position.character > 1) {
|
||
|
const preText = document.getText({
|
||
|
start: { line: position.line, character: 0 },
|
||
|
end: position,
|
||
|
});
|
||
|
return preText.match(/(\s|^)\.$/ig) === null;
|
||
|
}
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
exports.register = register;
|
||
|
function handleKindModifiers(item, tsEntry) {
|
||
|
if (tsEntry.kindModifiers) {
|
||
|
const kindModifiers = (0, modifiers_1.parseKindModifier)(tsEntry.kindModifiers);
|
||
|
if (kindModifiers.has(PConst.KindModifiers.optional)) {
|
||
|
if (!item.insertText) {
|
||
|
item.insertText = item.label;
|
||
|
}
|
||
|
if (!item.filterText) {
|
||
|
item.filterText = item.label;
|
||
|
}
|
||
|
item.label += '?';
|
||
|
}
|
||
|
if (kindModifiers.has(PConst.KindModifiers.deprecated)) {
|
||
|
item.tags = [1];
|
||
|
}
|
||
|
if (kindModifiers.has(PConst.KindModifiers.color)) {
|
||
|
item.kind = 16;
|
||
|
}
|
||
|
if (tsEntry.kind === PConst.Kind.script) {
|
||
|
for (const extModifier of PConst.KindModifiers.fileExtensionKindModifiers) {
|
||
|
if (kindModifiers.has(extModifier)) {
|
||
|
if (tsEntry.name.toLowerCase().endsWith(extModifier)) {
|
||
|
item.detail = tsEntry.name;
|
||
|
}
|
||
|
else {
|
||
|
item.detail = tsEntry.name + extModifier;
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
exports.handleKindModifiers = handleKindModifiers;
|
||
|
function rangeUnion(a, b) {
|
||
|
const start = (a.start.line < b.start.line || (a.start.line === b.start.line && a.start.character < b.start.character)) ? a.start : b.start;
|
||
|
const end = (a.end.line > b.end.line || (a.end.line === b.end.line && a.end.character > b.end.character)) ? a.end : b.end;
|
||
|
return { start, end };
|
||
|
}
|
||
|
//# sourceMappingURL=basic.js.map
|