This commit is contained in:
Orta Therox 2021-11-26 20:27:42 +01:00 committed by GitHub
commit 24210889e2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 182 additions and 59 deletions

View file

@ -12,20 +12,42 @@ import {
} from './languageModes';
import { getWordAtText, isWhitespaceOnly, repeat } from '../utils/strings';
import { HTMLDocumentRegions } from './embeddedSupport';
import { normalize, sep } from 'path';
import * as ts from 'typescript';
import { getSemanticTokens, getSemanticTokenLegend } from './javascriptSemanticTokens';
import { RequestService } from '../requests';
import { NodeRequestService } from '../node/nodeFs';
const JS_WORD_REGEX = /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g;
function getLanguageServiceHost(scriptKind: ts.ScriptKind) {
/** TypeScript does not handle schemes on file references, so normalize and remove the schemes when communicating with tsserver */
function deschemeURI(uri: string) {
if (!uri.startsWith('file://')) {
return uri;
}
// This function aims to replicate the logic in TypeScriptServiceClient.normalizedPath
let newPath = normalize(uri.replace('file://', ''));
// Both \ and / must be escaped in regular expressions
newPath = newPath.replace(new RegExp('\\' + sep, 'g'), '/');
if (process.platform !== 'win32') return newPath;
// Windows URIs come in like '/c%3A/Users/orta/dev/...', we need to switch it to 'c:/Users/orta/dev/...'
return newPath.slice(1).replace('%3A', ':');
}
function getLanguageServiceHost(scriptKind: ts.ScriptKind, fs: NodeRequestService) {
const compilerOptions: ts.CompilerOptions = { allowNonTsExtensions: true, allowJs: true, lib: ['lib.es6.d.ts'], target: ts.ScriptTarget.Latest, moduleResolution: ts.ModuleResolutionKind.Classic, experimentalDecorators: false };
let currentTextDocument = TextDocument.create('init', 'javascript', 1, '');
let currentWorkspace: Workspace = undefined!;
const jsLanguageService = import(/* webpackChunkName: "javascriptLibs" */ './javascriptLibs').then(libs => {
const host: ts.LanguageServiceHost = {
getCompilationSettings: () => compilerOptions,
getScriptFileNames: () => [currentTextDocument.uri, 'jquery'],
getScriptFileNames: () => [deschemeURI(currentTextDocument.uri), 'jquery'],
getScriptKind: (fileName) => {
if (fileName === currentTextDocument.uri) {
return scriptKind;
@ -33,15 +55,26 @@ function getLanguageServiceHost(scriptKind: ts.ScriptKind) {
return fileName.substr(fileName.length - 2) === 'ts' ? ts.ScriptKind.TS : ts.ScriptKind.JS;
},
getScriptVersion: (fileName: string) => {
if (fileName === currentTextDocument.uri) {
// Let the outer TextDocument give the version
if (fileName === deschemeURI(currentTextDocument.uri)) {
return String(currentTextDocument.version);
}
return '1'; // default lib an jquery.d.ts are static
// Default libs and jquery.d.ts are static.
// Include node_modules as a perf win
if (fileName.startsWith('lib.') || fileName === 'jquery' || fileName.includes('node_modules')) {
return '1';
}
// Unsure how this could occur, but better to not raise with statSync
if (currentWorkspace && !ts.sys.fileExists(fileName)) { return '1'; }
// Use mtime from the fs
return String(fs.statSync(fileName).mtime);
},
getScriptSnapshot: (fileName: string) => {
let text = '';
if (fileName === currentTextDocument.uri) {
if (fileName === deschemeURI(currentTextDocument.uri)) {
text = currentTextDocument.getText();
} else if (currentWorkspace && ts.sys.fileExists(fileName)) {
text = ts.sys.readFile(fileName, 'utf8')!;
} else {
text = libs.loadLibrary(fileName);
}
@ -51,14 +84,27 @@ function getLanguageServiceHost(scriptKind: ts.ScriptKind) {
getChangeRange: () => undefined
};
},
getCurrentDirectory: () => '',
getDefaultLibFileName: (_options: ts.CompilerOptions) => 'es6'
// Realistically the TSServer can only run on file:// workspaces, because it is entirely sync API
// and so `currentWorkspace` is only set when there is at least one root folder in a workspace with a file:// uri
getCurrentDirectory: () => {
const workspace = currentWorkspace && currentWorkspace.folders.find(ws => deschemeURI(currentTextDocument.uri).startsWith(deschemeURI(ws.uri)));
return workspace ? deschemeURI(workspace.uri) : '';
},
getDefaultLibFileName: (_options: ts.CompilerOptions) => 'es6',
fileExists: currentWorkspace && ts.sys.fileExists,
readFile: currentWorkspace && ts.sys.readFile,
readDirectory: currentWorkspace && ts.sys.readDirectory,
directoryExists: currentWorkspace && ts.sys.directoryExists,
getDirectories: currentWorkspace && ts.sys.getDirectories,
};
return ts.createLanguageService(host);
});
return {
async getLanguageService(jsDocument: TextDocument): Promise<ts.LanguageService> {
async getLanguageService(jsDocument: TextDocument, workspace: Workspace): Promise<ts.LanguageService> {
currentTextDocument = jsDocument;
if (workspace.folders.find(f => f.uri.startsWith('file://'))) currentWorkspace = workspace;
return jsLanguageService;
},
getCompilationSettings() {
@ -71,10 +117,10 @@ function getLanguageServiceHost(scriptKind: ts.ScriptKind) {
}
export function getJavaScriptMode(documentRegions: LanguageModelCache<HTMLDocumentRegions>, languageId: 'javascript' | 'typescript', workspace: Workspace): LanguageMode {
export function getJavaScriptMode(documentRegions: LanguageModelCache<HTMLDocumentRegions>, languageId: 'javascript' | 'typescript', workspace: Workspace, fs: RequestService): LanguageMode {
let jsDocuments = getLanguageModelCache<TextDocument>(10, 60, document => documentRegions.get(document).getEmbeddedDocument(languageId));
const host = getLanguageServiceHost(languageId === 'javascript' ? ts.ScriptKind.JS : ts.ScriptKind.TS);
const host = getLanguageServiceHost(languageId === 'javascript' ? ts.ScriptKind.JS : ts.ScriptKind.TS, fs as NodeRequestService);
let globalSettings: Settings = {};
return {
@ -84,9 +130,11 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache<HTMLDocume
async doValidation(document: TextDocument, settings = workspace.settings): Promise<Diagnostic[]> {
host.getCompilationSettings()['experimentalDecorators'] = settings && settings.javascript && settings.javascript.implicitProjectConfig.experimentalDecorators;
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);
const languageService = await host.getLanguageService(jsDocument, workspace);
const filePath = deschemeURI(jsDocument.uri);
const syntaxDiagnostics: ts.Diagnostic[] = languageService.getSyntacticDiagnostics(filePath);
const semanticDiagnostics = languageService.getSemanticDiagnostics(filePath);
return syntaxDiagnostics.concat(semanticDiagnostics).map((diag: ts.Diagnostic): Diagnostic => {
return {
range: convertRange(jsDocument, diag),
@ -98,9 +146,12 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache<HTMLDocume
},
async doComplete(document: TextDocument, position: Position, _documentContext: DocumentContext): Promise<CompletionList> {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
const jsLanguageService = await host.getLanguageService(jsDocument, workspace);
const filePath = deschemeURI(jsDocument.uri);
let offset = jsDocument.offsetAt(position);
let completions = jsLanguageService.getCompletionsAtPosition(jsDocument.uri, offset, { includeExternalModuleExports: false, includeInsertTextCompletions: false });
let completions = jsLanguageService.getCompletionsAtPosition(filePath, offset, { includeExternalModuleExports: false, includeInsertTextCompletions: false });
if (!completions) {
return { isIncomplete: false, items: [] };
}
@ -126,8 +177,10 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache<HTMLDocume
},
async doResolve(document: TextDocument, item: CompletionItem): Promise<CompletionItem> {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
let details = jsLanguageService.getCompletionEntryDetails(jsDocument.uri, item.data.offset, item.label, undefined, undefined, undefined, undefined);
const jsLanguageService = await host.getLanguageService(jsDocument, workspace);
const filePath = deschemeURI(jsDocument.uri);
let details = jsLanguageService.getCompletionEntryDetails(filePath, item.data.offset, item.label, undefined, undefined, undefined, undefined);
if (details) {
item.detail = ts.displayPartsToString(details.displayParts);
item.documentation = ts.displayPartsToString(details.documentation);
@ -137,8 +190,10 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache<HTMLDocume
},
async doHover(document: TextDocument, position: Position): Promise<Hover | null> {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
let info = jsLanguageService.getQuickInfoAtPosition(jsDocument.uri, jsDocument.offsetAt(position));
const jsLanguageService = await host.getLanguageService(jsDocument, workspace);
const filePath = deschemeURI(jsDocument.uri);
let info = jsLanguageService.getQuickInfoAtPosition(filePath, jsDocument.offsetAt(position));
if (info) {
const contents = ts.displayPartsToString(info.displayParts);
return {
@ -150,8 +205,10 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache<HTMLDocume
},
async doSignatureHelp(document: TextDocument, position: Position): Promise<SignatureHelp | null> {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
let signHelp = jsLanguageService.getSignatureHelpItems(jsDocument.uri, jsDocument.offsetAt(position), undefined);
const jsLanguageService = await host.getLanguageService(jsDocument, workspace);
const filePath = deschemeURI(jsDocument.uri);
let signHelp = jsLanguageService.getSignatureHelpItems(filePath, jsDocument.offsetAt(position), undefined);
if (signHelp) {
let ret: SignatureHelp = {
activeSignature: signHelp.selectedItemIndex,
@ -188,13 +245,15 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache<HTMLDocume
},
async doRename(document: TextDocument, position: Position, newName: string) {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
const jsLanguageService = await host.getLanguageService(jsDocument, workspace);
const jsDocumentPosition = jsDocument.offsetAt(position);
const { canRename } = jsLanguageService.getRenameInfo(jsDocument.uri, jsDocumentPosition);
const filePath = deschemeURI(jsDocument.uri);
const { canRename } = jsLanguageService.getRenameInfo(filePath, jsDocumentPosition);
if (!canRename) {
return null;
}
const renameInfos = jsLanguageService.findRenameLocations(jsDocument.uri, jsDocumentPosition, false, false);
const renameInfos = jsLanguageService.findRenameLocations(filePath, jsDocumentPosition, false, false);
const edits: TextEdit[] = [];
renameInfos?.map(renameInfo => {
@ -210,8 +269,10 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache<HTMLDocume
},
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 jsLanguageService = await host.getLanguageService(jsDocument, workspace);
const filePath = deschemeURI(jsDocument.uri);
const highlights = jsLanguageService.getDocumentHighlights(filePath, jsDocument.offsetAt(position), [filePath]);
const out: DocumentHighlight[] = [];
for (const entry of highlights || []) {
for (const highlight of entry.highlightSpans) {
@ -225,8 +286,10 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache<HTMLDocume
},
async findDocumentSymbols(document: TextDocument): Promise<SymbolInformation[]> {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
let items = jsLanguageService.getNavigationBarItems(jsDocument.uri);
const jsLanguageService = await host.getLanguageService(jsDocument, workspace);
const filePath = deschemeURI(jsDocument.uri);
let items = jsLanguageService.getNavigationBarItems(filePath);
if (items) {
let result: SymbolInformation[] = [];
let existing = Object.create(null);
@ -262,8 +325,10 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache<HTMLDocume
},
async findDefinition(document: TextDocument, position: Position): Promise<Definition | null> {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
let definition = jsLanguageService.getDefinitionAtPosition(jsDocument.uri, jsDocument.offsetAt(position));
const jsLanguageService = await host.getLanguageService(jsDocument, workspace);
const filePath = deschemeURI(jsDocument.uri);
let definition = jsLanguageService.getDefinitionAtPosition(filePath, jsDocument.offsetAt(position));
if (definition) {
return definition.filter(d => d.fileName === jsDocument.uri).map(d => {
return {
@ -276,10 +341,12 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache<HTMLDocume
},
async findReferences(document: TextDocument, position: Position): Promise<Location[]> {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
let references = jsLanguageService.getReferencesAtPosition(jsDocument.uri, jsDocument.offsetAt(position));
const jsLanguageService = await host.getLanguageService(jsDocument, workspace);
const filePath = deschemeURI(jsDocument.uri);
let references = jsLanguageService.getReferencesAtPosition(filePath, jsDocument.offsetAt(position));
if (references) {
return references.filter(d => d.fileName === jsDocument.uri).map(d => {
return references.filter(d => d.fileName === filePath).map(d => {
return {
uri: document.uri,
range: convertRange(jsDocument, d.textSpan)
@ -290,17 +357,20 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache<HTMLDocume
},
async getSelectionRange(document: TextDocument, position: Position): Promise<SelectionRange> {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
const jsLanguageService = await host.getLanguageService(jsDocument, workspace);
const filePath = deschemeURI(jsDocument.uri);
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));
const range = jsLanguageService.getSmartSelectionRange(filePath, 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 jsLanguageService = await host.getLanguageService(jsDocument, workspace);
const filePath = deschemeURI(jsDocument.uri);
let formatterSettings = settings && settings.javascript && settings.javascript.format;
@ -313,7 +383,7 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache<HTMLDocume
end -= range.end.character;
lastLineRange = Range.create(Position.create(range.end.line, 0), range.end);
}
let edits = jsLanguageService.getFormattingEditsForRange(jsDocument.uri, start, end, formatSettings);
let edits = jsLanguageService.getFormattingEditsForRange(filePath, start, end, formatSettings);
if (edits) {
let result = [];
for (let edit of edits) {
@ -336,8 +406,10 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache<HTMLDocume
},
async getFoldingRanges(document: TextDocument): Promise<FoldingRange[]> {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
let spans = jsLanguageService.getOutliningSpans(jsDocument.uri);
const jsLanguageService = await host.getLanguageService(jsDocument, workspace);
const filePath = deschemeURI(jsDocument.uri);
let spans = jsLanguageService.getOutliningSpans(filePath);
let ranges: FoldingRange[] = [];
for (let span of spans) {
let curr = convertRange(jsDocument, span.textSpan);
@ -359,8 +431,10 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache<HTMLDocume
},
async getSemanticTokens(document: TextDocument): Promise<SemanticTokenData[]> {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
return getSemanticTokens(jsLanguageService, jsDocument, jsDocument.uri);
const jsLanguageService = await host.getLanguageService(jsDocument, workspace);
const filePath = deschemeURI(jsDocument.uri);
return getSemanticTokens(jsLanguageService, jsDocument, filePath);
},
getSemanticTokenLegend(): { types: string[], modifiers: string[] } {
return getSemanticTokenLegend();

View file

@ -112,8 +112,8 @@ export function getLanguageModes(supportedLanguages: { [languageId: string]: boo
modes['css'] = getCSSMode(cssLanguageService, documentRegions, workspace);
}
if (supportedLanguages['javascript']) {
modes['javascript'] = getJavaScriptMode(documentRegions, 'javascript', workspace);
modes['typescript'] = getJavaScriptMode(documentRegions, 'typescript', workspace);
modes['javascript'] = getJavaScriptMode(documentRegions, 'javascript', workspace, requestService);
modes['typescript'] = getJavaScriptMode(documentRegions, 'typescript', workspace, requestService);
}
return {
async updateDataProviders(dataProviders: IHTMLDataProvider[]): Promise<void> {

View file

@ -8,13 +8,41 @@ import { URI as Uri } from 'vscode-uri';
import * as fs from 'fs';
import { FileType } from 'vscode-css-languageservice';
import { FileStat } from 'vscode-html-languageservice';
export function getNodeFSRequestService(): RequestService {
/**
* This extension is for the TSServer in the JavaScript mode which
* require sync access to stat's mtime due to TSServer API limitations
*/
export interface NodeRequestService extends RequestService {
statSync(location: string): FileStat
}
export function getNodeFSRequestService(): NodeRequestService {
function ensureFileUri(location: string) {
if (getScheme(location) !== 'file') {
throw new Error('fileRequestService can only handle file URLs');
}
}
const fsStatToFileStat = (stats: fs.Stats) => {
let type = FileType.Unknown;
if (stats.isFile()) {
type = FileType.File;
} else if (stats.isDirectory()) {
type = FileType.Directory;
} else if (stats.isSymbolicLink()) {
type = FileType.SymbolicLink;
}
return {
type,
ctime: stats.ctime.getTime(),
mtime: stats.mtime.getTime(),
size: stats.size
};
};
return {
getContent(location: string, encoding?: string) {
ensureFileUri(location);
@ -42,24 +70,16 @@ export function getNodeFSRequestService(): RequestService {
}
}
let type = FileType.Unknown;
if (stats.isFile()) {
type = FileType.File;
} else if (stats.isDirectory()) {
type = FileType.Directory;
} else if (stats.isSymbolicLink()) {
type = FileType.SymbolicLink;
}
c({
type,
ctime: stats.ctime.getTime(),
mtime: stats.mtime.getTime(),
size: stats.size
});
c(fsStatToFileStat(stats));
});
});
},
statSync(location: string) {
ensureFileUri(location);
const uri = Uri.parse(location);
const stats = fs.statSync(uri.fsPath);
return fsStatToFileStat(stats);
},
readDirectory(location: string) {
ensureFileUri(location);
return new Promise((c, e) => {

View file

@ -94,6 +94,21 @@ suite('HTML Completion', () => {
});
});
suite('JSDoc Imports', () => {
const fixtureRoot = path.resolve(__dirname, '../../src/test/jsdocImportFixtures');
const fixtureWorkspace = { name: 'fixture', uri: URI.file(fixtureRoot).toString() };
const indexHtmlUri = URI.file(path.resolve(fixtureRoot, 'index.html')).toString();
test('Imports across files', async () => {
await testCompletionFor('<html><script>/** @type {import("./jsDocTypes").SomeType } */\nconst a = {}; \n a.| \n</script><html>', {
items: [
{ label: 'other', },
{ label: 'property', },
]
}, indexHtmlUri, [fixtureWorkspace] );
});
});
suite('HTML Path Completion', () => {
const triggerSuggestCommand = {
title: 'Suggest',

View file

@ -0,0 +1,4 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

View file

@ -0,0 +1,9 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export type SomeType = {
property: string
other: number
};