Skip to content

Commit

Permalink
feat(language-server): support files that do not exist in FS but are …
Browse files Browse the repository at this point in the history
…open in the editor for TS project (#235)
  • Loading branch information
johnsoncodehk committed Aug 28, 2024
1 parent 26a4442 commit 0e1be44
Show file tree
Hide file tree
Showing 9 changed files with 200 additions and 93 deletions.
222 changes: 148 additions & 74 deletions packages/language-server/lib/project/typescriptProjectLs.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Language, LanguagePlugin, LanguageService, LanguageServiceEnvironment, ProjectContext, ProviderResult, createLanguage, createLanguageService, createUriMap } from '@volar/language-service';
import type { SnapshotDocument } from '../utils/snapshotDocument';
import { TypeScriptProjectHost, createLanguageServiceHost, createSys, resolveFileLanguageId } from '@volar/typescript';
import { matchFiles } from '@volar/typescript/lib/typescript/utilities';
import * as path from 'path-browserify';
import type * as ts from 'typescript';
import * as vscode from 'vscode-languageserver';
import { URI } from 'vscode-uri';
import type { LanguageServer } from '../types';
import type { SnapshotDocument } from '../utils/snapshotDocument';

export interface TypeScriptProjectLS {
tryAddFile(fileName: string): void;
Expand Down Expand Up @@ -57,7 +58,7 @@ export async function createTypeScriptLS(
return projectVersion.toString();
},
getScriptFileNames() {
return rootFiles;
return commandLine.fileNames;
},
getCompilationSettings() {
return commandLine.options;
Expand All @@ -74,12 +75,44 @@ export async function createTypeScriptLS(
sys,
uriConverter,
});
const docOpenWatcher = server.documents.onDidOpen(({ document }) => updateFsCacheFromSyncedDocument(document));
const docSaveWatcher = server.documents.onDidSave(({ document }) => updateFsCacheFromSyncedDocument(document));
const docChangeWatcher = server.documents.onDidChangeContent(() => projectVersion++);
const fileChangeWatcher = serviceEnv.onDidChangeWatchedFiles?.(params => onWorkspaceFilesChanged(params.changes));
const unsavedRootFileUris = createUriMap();
const disposables = [
server.documents.onDidOpen(({ document }) => updateFsCacheFromSyncedDocument(document)),
server.documents.onDidSave(({ document }) => updateFsCacheFromSyncedDocument(document)),
server.documents.onDidChangeContent(() => projectVersion++),
serviceEnv.onDidChangeWatchedFiles?.(async ({ changes }) => {
const createdOrDeleted = changes.some(change => change.type !== vscode.FileChangeType.Changed);
if (createdOrDeleted) {
await updateCommandLine();
}
projectVersion++;
}),
server.documents.onDidOpen(async ({ document }) => {
const uri = URI.parse(document.uri);
const isWorkspaceFile = workspaceFolder.scheme === uri.scheme;
if (!isWorkspaceFile) {
return;
}
const stat = await serviceEnv.fs?.stat(uri);
const isUnsaved = stat?.type !== 1;
if (isUnsaved) {
const lastProjectVersion = projectVersion;
await updateCommandLine();
if (lastProjectVersion !== projectVersion) {
unsavedRootFileUris.set(uri, true);
}
}
}),
server.documents.onDidClose(async ({ document }) => {
const uri = URI.parse(document.uri);
if (unsavedRootFileUris.has(uri)) {
unsavedRootFileUris.delete(uri);
await updateCommandLine();
}
}),
].filter(d => !!d);

let rootFiles = await getRootFiles(languagePlugins);
await updateCommandLine();

const language = createLanguage<URI>(
[
Expand Down Expand Up @@ -146,18 +179,16 @@ export async function createTypeScriptLS(
return {
languageService,
tryAddFile(fileName: string) {
if (!rootFiles.includes(fileName)) {
rootFiles.push(fileName);
if (!commandLine.fileNames.includes(fileName)) {
commandLine.fileNames.push(fileName);
projectVersion++;
}
},
dispose: () => {
sys.dispose();
languageService?.dispose();
fileChangeWatcher?.dispose();
docOpenWatcher.dispose();
docSaveWatcher.dispose();
docChangeWatcher.dispose();
disposables.forEach(({ dispose }) => dispose());
disposables.length = 0;
},
getCommandLine: () => commandLine,
};
Expand All @@ -171,78 +202,121 @@ export async function createTypeScriptLS(
}
}

async function onWorkspaceFilesChanged(changes: vscode.FileEvent[]) {

const createsAndDeletes = changes.filter(change => change.type !== vscode.FileChangeType.Changed);

if (createsAndDeletes.length) {
rootFiles = await getRootFiles(languagePlugins);
}

projectVersion++;
}

async function getRootFiles(languagePlugins: LanguagePlugin<URI>[]) {
async function updateCommandLine() {
const oldFileNames = new Set(commandLine?.fileNames ?? []);
commandLine = await parseConfig(
ts,
sys,
uriConverter.asFileName(workspaceFolder),
tsconfig,
languagePlugins.map(plugin => plugin.typescript?.extraFileExtensions ?? []).flat()
);
return commandLine.fileNames;
const newFileNames = new Set(commandLine.fileNames);
if (oldFileNames.size !== newFileNames.size || [...oldFileNames].some(fileName => !newFileNames.has(fileName))) {
projectVersion++;
}
}
}

async function parseConfig(
ts: typeof import('typescript'),
sys: ReturnType<typeof createSys>,
workspacePath: string,
tsconfig: string | ts.CompilerOptions,
extraFileExtensions: ts.FileExtensionInfo[]
) {
let commandLine: ts.ParsedCommandLine = {
errors: [],
fileNames: [],
options: {},
};
let sysVersion: number | undefined;
let newSysVersion = await sys.sync();
while (sysVersion !== newSysVersion) {
sysVersion = newSysVersion;
try {
commandLine = await parseConfigWorker(ts, sys, workspacePath, tsconfig, extraFileExtensions);
} catch {
// will be failed if web fs host first result not ready
async function parseConfig(
ts: typeof import('typescript'),
sys: ReturnType<typeof createSys>,
workspacePath: string,
tsconfig: string | ts.CompilerOptions,
extraFileExtensions: ts.FileExtensionInfo[]
) {
let commandLine: ts.ParsedCommandLine = {
errors: [],
fileNames: [],
options: {},
};
let sysVersion: number | undefined;
let newSysVersion = await sys.sync();
while (sysVersion !== newSysVersion) {
sysVersion = newSysVersion;
try {
commandLine = await parseConfigWorker(ts, sys, workspacePath, tsconfig, extraFileExtensions);
} catch {
// will be failed if web fs host first result not ready
}
newSysVersion = await sys.sync();
}
newSysVersion = await sys.sync();
return commandLine;
}
return commandLine;
}

function parseConfigWorker(
ts: typeof import('typescript'),
host: ts.ParseConfigHost,
workspacePath: string,
tsconfig: string | ts.CompilerOptions,
extraFileExtensions: ts.FileExtensionInfo[]
) {
let content: ts.ParsedCommandLine = {
errors: [],
fileNames: [],
options: {},
};
if (typeof tsconfig === 'string') {
const config = ts.readJsonConfigFile(tsconfig, host.readFile);
content = ts.parseJsonSourceFileConfigFileContent(config, host, path.dirname(tsconfig), {}, tsconfig, undefined, extraFileExtensions);
}
else {
content = ts.parseJsonConfigFileContent({ files: [] }, host, workspacePath, tsconfig, workspacePath + '/jsconfig.json', undefined, extraFileExtensions);
function parseConfigWorker(
ts: typeof import('typescript'),
_host: ts.ParseConfigHost,
workspacePath: string,
tsconfig: string | ts.CompilerOptions,
extraFileExtensions: ts.FileExtensionInfo[]
) {
let content: ts.ParsedCommandLine = {
errors: [],
fileNames: [],
options: {},
};
const maybeUnsavedFileNames = server.documents.all()
.map(document => URI.parse(document.uri))
.filter(uri => uri.scheme === workspaceFolder.scheme)
.map(uri => uriConverter.asFileName(uri));
const host: ts.ParseConfigHost = {
..._host,
readDirectory(rootDir, extensions, excludes, includes, depth) {
const fsFiles = _host.readDirectory(rootDir, extensions, excludes, includes, depth);
const unsavedFiles = matchFiles(
rootDir,
extensions,
excludes,
includes,
sys.useCaseSensitiveFileNames,
getCurrentDirectory(),
depth,
dirPath => {
dirPath = dirPath.replace(/\\/g, '/');
const files: string[] = [];
const dirs: string[] = [];
for (const fileName of maybeUnsavedFileNames) {
const match = sys.useCaseSensitiveFileNames
? fileName.startsWith(dirPath + '/')
: fileName.toLowerCase().startsWith(dirPath.toLowerCase() + '/');
if (match) {
const name = fileName.slice(dirPath.length + 1);
if (name.includes('/')) {
const dir = name.split('/')[0];
if (!dirs.includes(dir)) {
dirs.push(dir);
}
}
else {
files.push(name);
}
}
}
return {
files,
directories: dirs,
};
},
path => path
);
if (!unsavedFiles.length) {
return fsFiles;
}
return [...new Set([...fsFiles, ...unsavedFiles])];
},
};
if (typeof tsconfig === 'string') {
const config = ts.readJsonConfigFile(tsconfig, host.readFile);
content = ts.parseJsonSourceFileConfigFileContent(config, host, path.dirname(tsconfig), {}, tsconfig, undefined, extraFileExtensions);
}
else {
content = ts.parseJsonConfigFileContent({ files: [] }, host, workspacePath, tsconfig, workspacePath + '/jsconfig.json', undefined, extraFileExtensions);
}
// fix https://github.com/johnsoncodehk/volar/issues/1786
// https://github.com/microsoft/TypeScript/issues/30457
// patching ts server broke with outDir + rootDir + composite/incremental
content.options.outDir = undefined;
content.fileNames = content.fileNames.map(fileName => fileName.replace(/\\/g, '/'));
return content;
}
// fix https://github.com/johnsoncodehk/volar/issues/1786
// https://github.com/microsoft/TypeScript/issues/30457
// patching ts server broke with outDir + rootDir + composite/incremental
content.options.outDir = undefined;
content.fileNames = content.fileNames.map(fileName => fileName.replace(/\\/g, '/'));
return content;
}
4 changes: 2 additions & 2 deletions packages/language-service/lib/features/resolveCodeAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@ export function register(context: LanguageServiceContext) {
return async (item: vscode.CodeAction, token = NoneCancellationToken) => {

const data: ServiceCodeActionData | undefined = item.data;
delete item.data;

if (data) {

const plugin = context.plugins[data.pluginIndex];
if (!plugin[1].resolveCodeAction) {
delete item.data;
return item;
}

Expand All @@ -37,6 +36,7 @@ export function register(context: LanguageServiceContext) {
);
}

delete item.data;
return item;
};
}
7 changes: 3 additions & 4 deletions packages/language-service/lib/features/resolveCodeLens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,11 @@ export function register(context: LanguageServiceContext) {
return async (item: vscode.CodeLens, token = NoneCancellationToken) => {

const data: ServiceCodeLensData | ServiceReferencesCodeLensData | undefined = item.data;
delete item.data;

if (data?.kind === 'normal') {

const plugin = context.plugins[data.pluginIndex];
if (!plugin[1].resolveCodeLens) {
delete item.data;
return item;
}

Expand All @@ -26,14 +25,14 @@ export function register(context: LanguageServiceContext) {

// item.range already transformed in codeLens request
}

if (data?.kind === 'references') {
else if (data?.kind === 'references') {

const references = await findReferences(URI.parse(data.sourceFileUri), item.range.start, { includeDeclaration: false }, token) ?? [];

item.command = context.commands.showReferences.create(data.sourceFileUri, item.range.start, references);
}

delete item.data;
return item;
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,12 @@ export function register(context: LanguageServiceContext) {
return async (item: vscode.CompletionItem, token = NoneCancellationToken) => {

const data: ServiceCompletionData | undefined = item.data;
delete item.data;

if (data) {

const plugin = context.plugins[data.pluginIndex];

if (!plugin[1].resolveCompletionItem) {
delete item.data;
return item;
}

Expand Down Expand Up @@ -59,6 +58,7 @@ export function register(context: LanguageServiceContext) {
item.detail = item.detail;
}

delete item.data;
return item;
};
}
4 changes: 2 additions & 2 deletions packages/language-service/lib/features/resolveDocumentLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,10 @@ export function register(context: LanguageServiceContext) {
return async (item: vscode.DocumentLink, token = NoneCancellationToken) => {

const data: DocumentLinkData | undefined = item.data;
delete item.data;

if (data) {
const plugin = context.plugins[data.pluginIndex];
if (!plugin[1].resolveDocumentLink) {
delete item.data;
return item;
}

Expand All @@ -25,6 +24,7 @@ export function register(context: LanguageServiceContext) {
}
}

delete item.data;
return item;
};
}
4 changes: 2 additions & 2 deletions packages/language-service/lib/features/resolveInlayHint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,18 @@ export function register(context: LanguageServiceContext) {
return async (item: vscode.InlayHint, token = NoneCancellationToken) => {

const data: InlayHintData | undefined = item.data;
delete item.data;

if (data) {
const plugin = context.plugins[data.pluginIndex];
if (!plugin[1].resolveInlayHint) {
delete item.data;
return item;
}

Object.assign(item, data.original);
item = await plugin[1].resolveInlayHint(item, token);
}

delete item.data;
return item;
};
}
Loading

0 comments on commit 0e1be44

Please sign in to comment.