Files
vscode/extensions/html-language-features/server/src/modes/javascriptMode.ts
2025-03-28 22:48:10 +08:00

629 lines
24 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { LanguageModelCache, getLanguageModelCache } from '../languageModelCache';
import {
SymbolInformation, SymbolKind, CompletionItem, Location, SignatureHelp, SignatureInformation, ParameterInformation,
Definition, TextEdit, TextDocument, Diagnostic, DiagnosticSeverity, Range, CompletionItemKind, Hover,
DocumentHighlight, DocumentHighlightKind, CompletionList, Position, FormattingOptions, FoldingRange, FoldingRangeKind, SelectionRange,
LanguageMode, Settings, SemanticTokenData, Workspace, DocumentContext, CompletionItemData, isCompletionItemData, Uri, FILE_PROTOCOL
} from './languageModes';
import { getWordAtText, isWhitespaceOnly, repeat } from '../utils/strings';
import { HTMLDocumentRegions } from './embeddedSupport';
import * as ts from 'typescript';
import { getSemanticTokens, getSemanticTokenLegend } from './javascriptSemanticTokens';
const JS_WORD_REGEX = /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g;
function getLanguageServiceHost(scriptKind: ts.ScriptKind) {
const compilerOptions: ts.CompilerOptions = { allowNonTsExtensions: true, allowJs: true, lib: ['lib.es2020.full.d.ts'], target: ts.ScriptTarget.Latest, moduleResolution: ts.ModuleResolutionKind.Classic, experimentalDecorators: false };
let currentTextDocument = TextDocument.create('init', 'javascript', 1, '');
const jsLanguageService = import(/* webpackChunkName: "javascriptLibs" */ './javascriptLibs.js').then(libs => {
const host: ts.LanguageServiceHost = {
getCompilationSettings: () => compilerOptions,
getScriptFileNames: () => [currentTextDocument.uri, 'jquery'],
getScriptKind: (fileName) => {
if (fileName === currentTextDocument.uri) {
return scriptKind;
}
return fileName.substr(fileName.length - 2) === 'ts' ? ts.ScriptKind.TS : ts.ScriptKind.JS;
},
getScriptVersion: (fileName: string) => {
if (fileName === currentTextDocument.uri) {
return String(currentTextDocument.version);
}
return '1'; // default lib an jquery.d.ts are static
},
getScriptSnapshot: (fileName: string) => {
let text = '';
if (fileName === currentTextDocument.uri) {
text = currentTextDocument.getText();
} else {
text = libs.loadLibrary(fileName);
}
return {
getText: (start, end) => text.substring(start, end),
getLength: () => text.length,
getChangeRange: () => undefined
};
},
getCurrentDirectory: () => '',
getDefaultLibFileName: (_options: ts.CompilerOptions) => 'es2020.full',
readFile: (path: string, _encoding?: string | undefined): string | undefined => {
if (path === currentTextDocument.uri) {
return currentTextDocument.getText();
} else {
return libs.loadLibrary(path);
}
},
fileExists: (path: string): boolean => {
if (path === currentTextDocument.uri) {
return true;
} else {
return !!libs.loadLibrary(path);
}
},
directoryExists: (path: string): boolean => {
// typescript tries to first find libraries in node_modules/@types and node_modules/@typescript
// there's no node_modules in our setup
if (path.startsWith('node_modules')) {
return false;
}
return true;
}
};
return {
service: ts.createLanguageService(host),
loadLibrary: libs.loadLibrary,
};
});
return {
async getLanguageService(jsDocument: TextDocument): Promise<ts.LanguageService> {
currentTextDocument = jsDocument;
return (await jsLanguageService).service;
},
getCompilationSettings() {
return compilerOptions;
},
async loadLibrary(fileName: string) {
return (await jsLanguageService).loadLibrary(fileName);
},
dispose() {
jsLanguageService.then(s => s.service.dispose());
}
};
}
const ignoredErrors = [
1108, /* A_return_statement_can_only_be_used_within_a_function_body_1108 */
2792, /* Cannot_find_module_0_Did_you_mean_to_set_the_moduleResolution_option_to_node_or_to_add_aliases_to_the_paths_option */
];
export function getJavaScriptMode(documentRegions: LanguageModelCache<HTMLDocumentRegions>, languageId: 'javascript' | 'typescript', workspace: Workspace, extensionUri?: Uri): LanguageMode {
const jsDocuments = getLanguageModelCache<TextDocument>(10, 60, document => documentRegions.get(document).getEmbeddedDocument(languageId));
const host = getLanguageServiceHost(languageId === 'javascript' ? ts.ScriptKind.JS : ts.ScriptKind.TS);
const globalSettings: Settings = {};
function updateHostSettings(settings: Settings) {
const hostSettings = host.getCompilationSettings();
hostSettings.experimentalDecorators = settings?.['js/ts']?.implicitProjectConfig?.experimentalDecorators;
hostSettings.strictNullChecks = settings?.['js/ts']?.implicitProjectConfig.strictNullChecks;
}
return {
getId() {
return languageId;
},
async doValidation(document: TextDocument, settings = workspace.settings): Promise<Diagnostic[]> {
updateHostSettings(settings);
const jsDocument = jsDocuments.get(document);
const languageService = await host.getLanguageService(jsDocument);
const syntaxDiagnostics: ts.Diagnostic[] = languageService.getSyntacticDiagnostics(jsDocument.uri);
const semanticDiagnostics = languageService.getSemanticDiagnostics(jsDocument.uri);
return syntaxDiagnostics.concat(semanticDiagnostics).filter(d => !ignoredErrors.includes(d.code)).map((diag: ts.Diagnostic): Diagnostic => {
return {
range: convertRange(jsDocument, diag),
severity: DiagnosticSeverity.Error,
source: languageId,
message: ts.flattenDiagnosticMessageText(diag.messageText, '\n')
};
});
},
async doComplete(document: TextDocument, position: Position, _documentContext: DocumentContext): Promise<CompletionList> {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
const offset = jsDocument.offsetAt(position);
const completions = jsLanguageService.getCompletionsAtPosition(jsDocument.uri, offset, { includeExternalModuleExports: false, includeInsertTextCompletions: false });
if (!completions) {
return { isIncomplete: false, items: [] };
}
const replaceRange = convertRange(jsDocument, getWordAtText(jsDocument.getText(), offset, JS_WORD_REGEX));
return {
isIncomplete: false,
items: completions.entries.map(entry => {
const data: CompletionItemData = { // data used for resolving item details (see 'doResolve')
languageId,
uri: document.uri,
offset: offset
};
return {
uri: document.uri,
position: position,
label: entry.name,
sortText: entry.sortText,
kind: convertKind(entry.kind),
textEdit: TextEdit.replace(replaceRange, entry.name),
data
};
})
};
},
async doResolve(document: TextDocument, item: CompletionItem): Promise<CompletionItem> {
if (isCompletionItemData(item.data)) {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
const details = jsLanguageService.getCompletionEntryDetails(jsDocument.uri, item.data.offset, item.label, undefined, undefined, undefined, undefined);
if (details) {
item.detail = ts.displayPartsToString(details.displayParts);
item.documentation = ts.displayPartsToString(details.documentation);
delete item.data;
}
}
return item;
},
async doHover(document: TextDocument, position: Position): Promise<Hover | null> {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
const info = jsLanguageService.getQuickInfoAtPosition(jsDocument.uri, jsDocument.offsetAt(position));
if (info) {
const contents = ts.displayPartsToString(info.displayParts);
return {
range: convertRange(jsDocument, info.textSpan),
contents: ['```typescript', contents, '```'].join('\n')
};
}
return null;
},
async doSignatureHelp(document: TextDocument, position: Position): Promise<SignatureHelp | null> {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
const signHelp = jsLanguageService.getSignatureHelpItems(jsDocument.uri, jsDocument.offsetAt(position), undefined);
if (signHelp) {
const ret: SignatureHelp = {
activeSignature: signHelp.selectedItemIndex,
activeParameter: signHelp.argumentIndex,
signatures: []
};
signHelp.items.forEach(item => {
const signature: SignatureInformation = {
label: '',
documentation: undefined,
parameters: []
};
signature.label += ts.displayPartsToString(item.prefixDisplayParts);
item.parameters.forEach((p, i, a) => {
const label = ts.displayPartsToString(p.displayParts);
const parameter: ParameterInformation = {
label: label,
documentation: ts.displayPartsToString(p.documentation)
};
signature.label += label;
signature.parameters!.push(parameter);
if (i < a.length - 1) {
signature.label += ts.displayPartsToString(item.separatorDisplayParts);
}
});
signature.label += ts.displayPartsToString(item.suffixDisplayParts);
ret.signatures.push(signature);
});
return ret;
}
return null;
},
async doRename(document: TextDocument, position: Position, newName: string) {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
const jsDocumentPosition = jsDocument.offsetAt(position);
const { canRename } = jsLanguageService.getRenameInfo(jsDocument.uri, jsDocumentPosition);
if (!canRename) {
return null;
}
const renameInfos = jsLanguageService.findRenameLocations(jsDocument.uri, jsDocumentPosition, false, false);
const edits: TextEdit[] = [];
renameInfos?.map(renameInfo => {
edits.push({
range: convertRange(jsDocument, renameInfo.textSpan),
newText: newName,
});
});
return {
changes: { [document.uri]: edits },
};
},
async findDocumentHighlight(document: TextDocument, position: Position): Promise<DocumentHighlight[]> {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
const highlights = jsLanguageService.getDocumentHighlights(jsDocument.uri, jsDocument.offsetAt(position), [jsDocument.uri]);
const out: DocumentHighlight[] = [];
for (const entry of highlights || []) {
for (const highlight of entry.highlightSpans) {
out.push({
range: convertRange(jsDocument, highlight.textSpan),
kind: highlight.kind === 'writtenReference' ? DocumentHighlightKind.Write : DocumentHighlightKind.Text
});
}
}
return out;
},
async findDocumentSymbols(document: TextDocument): Promise<SymbolInformation[]> {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
const items = jsLanguageService.getNavigationBarItems(jsDocument.uri);
if (items) {
const result: SymbolInformation[] = [];
const existing = Object.create(null);
const collectSymbols = (item: ts.NavigationBarItem, containerLabel?: string) => {
const sig = item.text + item.kind + item.spans[0].start;
if (item.kind !== 'script' && !existing[sig]) {
const symbol: SymbolInformation = {
name: item.text,
kind: convertSymbolKind(item.kind),
location: {
uri: document.uri,
range: convertRange(jsDocument, item.spans[0])
},
containerName: containerLabel
};
existing[sig] = true;
result.push(symbol);
containerLabel = item.text;
}
if (item.childItems && item.childItems.length > 0) {
for (const child of item.childItems) {
collectSymbols(child, containerLabel);
}
}
};
items.forEach(item => collectSymbols(item));
return result;
}
return [];
},
async findDefinition(document: TextDocument, position: Position): Promise<Definition | null> {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
const definition = jsLanguageService.getDefinitionAtPosition(jsDocument.uri, jsDocument.offsetAt(position));
if (definition) {
return (await Promise.all(definition.map(async d => {
if (d.fileName === jsDocument.uri) {
return {
uri: document.uri,
range: convertRange(jsDocument, d.textSpan)
};
} else {
if (!extensionUri) {
return undefined;
}
const libUri = `${FILE_PROTOCOL}://${languageId}/${d.fileName}`;
const content = await host.loadLibrary(d.fileName);
if (!content) {
return undefined;
}
const libDocument = TextDocument.create(libUri, languageId, 1, content);
return {
uri: libUri,
range: convertRange(libDocument, d.textSpan)
};
}
}))).filter(d => !!d);
}
return null;
},
async findReferences(document: TextDocument, position: Position): Promise<Location[]> {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
const references = jsLanguageService.getReferencesAtPosition(jsDocument.uri, jsDocument.offsetAt(position));
if (references) {
return references.filter(d => d.fileName === jsDocument.uri).map(d => {
return {
uri: document.uri,
range: convertRange(jsDocument, d.textSpan)
};
});
}
return [];
},
async getSelectionRange(document: TextDocument, position: Position): Promise<SelectionRange> {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
function convertSelectionRange(selectionRange: ts.SelectionRange): SelectionRange {
const parent = selectionRange.parent ? convertSelectionRange(selectionRange.parent) : undefined;
return SelectionRange.create(convertRange(jsDocument, selectionRange.textSpan), parent);
}
const range = jsLanguageService.getSmartSelectionRange(jsDocument.uri, jsDocument.offsetAt(position));
return convertSelectionRange(range);
},
async format(document: TextDocument, range: Range, formatParams: FormattingOptions, settings: Settings = globalSettings): Promise<TextEdit[]> {
const jsDocument = documentRegions.get(document).getEmbeddedDocument('javascript', true);
const jsLanguageService = await host.getLanguageService(jsDocument);
const formatterSettings = settings && settings.javascript && settings.javascript.format;
const initialIndentLevel = computeInitialIndent(document, range, formatParams);
const formatSettings = convertOptions(formatParams, formatterSettings, initialIndentLevel + 1);
const start = jsDocument.offsetAt(range.start);
let end = jsDocument.offsetAt(range.end);
let lastLineRange = null;
if (range.end.line > range.start.line && (range.end.character === 0 || isWhitespaceOnly(jsDocument.getText().substr(end - range.end.character, range.end.character)))) {
end -= range.end.character;
lastLineRange = Range.create(Position.create(range.end.line, 0), range.end);
}
const edits = jsLanguageService.getFormattingEditsForRange(jsDocument.uri, start, end, formatSettings);
if (edits) {
const result = [];
for (const edit of edits) {
if (edit.span.start >= start && edit.span.start + edit.span.length <= end) {
result.push({
range: convertRange(jsDocument, edit.span),
newText: edit.newText
});
}
}
if (lastLineRange) {
result.push({
range: lastLineRange,
newText: generateIndent(initialIndentLevel, formatParams)
});
}
return result;
}
return [];
},
async getFoldingRanges(document: TextDocument): Promise<FoldingRange[]> {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
const spans = jsLanguageService.getOutliningSpans(jsDocument.uri);
const ranges: FoldingRange[] = [];
for (const span of spans) {
const curr = convertRange(jsDocument, span.textSpan);
const startLine = curr.start.line;
const endLine = curr.end.line;
if (startLine < endLine) {
const foldingRange: FoldingRange = { startLine, endLine };
const match = document.getText(curr).match(/^\s*\/(?:(\/\s*#(?:end)?region\b)|(\*|\/))/);
if (match) {
foldingRange.kind = match[1] ? FoldingRangeKind.Region : FoldingRangeKind.Comment;
}
ranges.push(foldingRange);
}
}
return ranges;
},
onDocumentRemoved(document: TextDocument) {
jsDocuments.onDocumentRemoved(document);
},
async getSemanticTokens(document: TextDocument): Promise<SemanticTokenData[]> {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
return [...getSemanticTokens(jsLanguageService, jsDocument, jsDocument.uri)];
},
getSemanticTokenLegend(): { types: string[]; modifiers: string[] } {
return getSemanticTokenLegend();
},
getTextDocumentContent(name: string): Promise<string> {
return host.loadLibrary(name);
},
dispose() {
host.dispose();
jsDocuments.dispose();
}
};
}
function convertRange(document: TextDocument, span: { start: number | undefined; length: number | undefined }): Range {
if (typeof span.start === 'undefined') {
const pos = document.positionAt(0);
return Range.create(pos, pos);
}
const startPosition = document.positionAt(span.start);
const endPosition = document.positionAt(span.start + (span.length || 0));
return Range.create(startPosition, endPosition);
}
function convertKind(kind: string): CompletionItemKind {
switch (kind) {
case Kind.primitiveType:
case Kind.keyword:
return CompletionItemKind.Keyword;
case Kind.const:
case Kind.let:
case Kind.variable:
case Kind.localVariable:
case Kind.alias:
case Kind.parameter:
return CompletionItemKind.Variable;
case Kind.memberVariable:
case Kind.memberGetAccessor:
case Kind.memberSetAccessor:
return CompletionItemKind.Field;
case Kind.function:
case Kind.localFunction:
return CompletionItemKind.Function;
case Kind.method:
case Kind.constructSignature:
case Kind.callSignature:
case Kind.indexSignature:
return CompletionItemKind.Method;
case Kind.enum:
return CompletionItemKind.Enum;
case Kind.enumMember:
return CompletionItemKind.EnumMember;
case Kind.module:
case Kind.externalModuleName:
return CompletionItemKind.Module;
case Kind.class:
case Kind.type:
return CompletionItemKind.Class;
case Kind.interface:
return CompletionItemKind.Interface;
case Kind.warning:
return CompletionItemKind.Text;
case Kind.script:
return CompletionItemKind.File;
case Kind.directory:
return CompletionItemKind.Folder;
case Kind.string:
return CompletionItemKind.Constant;
default:
return CompletionItemKind.Property;
}
}
const enum Kind {
alias = 'alias',
callSignature = 'call',
class = 'class',
const = 'const',
constructorImplementation = 'constructor',
constructSignature = 'construct',
directory = 'directory',
enum = 'enum',
enumMember = 'enum member',
externalModuleName = 'external module name',
function = 'function',
indexSignature = 'index',
interface = 'interface',
keyword = 'keyword',
let = 'let',
localFunction = 'local function',
localVariable = 'local var',
method = 'method',
memberGetAccessor = 'getter',
memberSetAccessor = 'setter',
memberVariable = 'property',
module = 'module',
primitiveType = 'primitive type',
script = 'script',
type = 'type',
variable = 'var',
warning = 'warning',
string = 'string',
parameter = 'parameter',
typeParameter = 'type parameter'
}
function convertSymbolKind(kind: string): SymbolKind {
switch (kind) {
case Kind.module: return SymbolKind.Module;
case Kind.class: return SymbolKind.Class;
case Kind.enum: return SymbolKind.Enum;
case Kind.enumMember: return SymbolKind.EnumMember;
case Kind.interface: return SymbolKind.Interface;
case Kind.indexSignature: return SymbolKind.Method;
case Kind.callSignature: return SymbolKind.Method;
case Kind.method: return SymbolKind.Method;
case Kind.memberVariable: return SymbolKind.Property;
case Kind.memberGetAccessor: return SymbolKind.Property;
case Kind.memberSetAccessor: return SymbolKind.Property;
case Kind.variable: return SymbolKind.Variable;
case Kind.let: return SymbolKind.Variable;
case Kind.const: return SymbolKind.Variable;
case Kind.localVariable: return SymbolKind.Variable;
case Kind.alias: return SymbolKind.Variable;
case Kind.function: return SymbolKind.Function;
case Kind.localFunction: return SymbolKind.Function;
case Kind.constructSignature: return SymbolKind.Constructor;
case Kind.constructorImplementation: return SymbolKind.Constructor;
case Kind.typeParameter: return SymbolKind.TypeParameter;
case Kind.string: return SymbolKind.String;
default: return SymbolKind.Variable;
}
}
function convertOptions(options: FormattingOptions, formatSettings: any, initialIndentLevel: number): ts.FormatCodeSettings {
return {
convertTabsToSpaces: options.insertSpaces,
tabSize: options.tabSize,
indentSize: options.tabSize,
indentStyle: ts.IndentStyle.Smart,
newLineCharacter: '\n',
baseIndentSize: options.tabSize * initialIndentLevel,
insertSpaceAfterCommaDelimiter: Boolean(!formatSettings || formatSettings.insertSpaceAfterCommaDelimiter),
insertSpaceAfterConstructor: Boolean(formatSettings && formatSettings.insertSpaceAfterConstructor),
insertSpaceAfterSemicolonInForStatements: Boolean(!formatSettings || formatSettings.insertSpaceAfterSemicolonInForStatements),
insertSpaceBeforeAndAfterBinaryOperators: Boolean(!formatSettings || formatSettings.insertSpaceBeforeAndAfterBinaryOperators),
insertSpaceAfterKeywordsInControlFlowStatements: Boolean(!formatSettings || formatSettings.insertSpaceAfterKeywordsInControlFlowStatements),
insertSpaceAfterFunctionKeywordForAnonymousFunctions: Boolean(!formatSettings || formatSettings.insertSpaceAfterFunctionKeywordForAnonymousFunctions),
insertSpaceBeforeFunctionParenthesis: Boolean(formatSettings && formatSettings.insertSpaceBeforeFunctionParenthesis),
insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis),
insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets),
insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces),
insertSpaceAfterOpeningAndBeforeClosingEmptyBraces: Boolean(!formatSettings || formatSettings.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces),
insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces),
insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces),
insertSpaceAfterTypeAssertion: Boolean(formatSettings && formatSettings.insertSpaceAfterTypeAssertion),
placeOpenBraceOnNewLineForControlBlocks: Boolean(formatSettings && formatSettings.placeOpenBraceOnNewLineForFunctions),
placeOpenBraceOnNewLineForFunctions: Boolean(formatSettings && formatSettings.placeOpenBraceOnNewLineForControlBlocks),
semicolons: formatSettings?.semicolons
};
}
function computeInitialIndent(document: TextDocument, range: Range, options: FormattingOptions) {
const lineStart = document.offsetAt(Position.create(range.start.line, 0));
const content = document.getText();
let i = lineStart;
let nChars = 0;
const tabSize = options.tabSize || 4;
while (i < content.length) {
const ch = content.charAt(i);
if (ch === ' ') {
nChars++;
} else if (ch === '\t') {
nChars += tabSize;
} else {
break;
}
i++;
}
return Math.floor(nChars / tabSize);
}
function generateIndent(level: number, options: FormattingOptions) {
if (options.insertSpaces) {
return repeat(' ', level * options.tabSize);
} else {
return repeat('\t', level);
}
}