Skip to content

Commit

Permalink
feat: Full web support (#47)
Browse files Browse the repository at this point in the history
  • Loading branch information
johnsoncodehk committed Jun 2, 2023
1 parent 130e823 commit b0ac35c
Show file tree
Hide file tree
Showing 58 changed files with 2,113 additions and 2,345 deletions.
9 changes: 3 additions & 6 deletions packages/kit/README.md
Expand Up @@ -31,11 +31,11 @@ createWatcher(path.dirname(tsconfig), ['ts', 'js', 'foo'])
})
.on('unlink', (fileName) => {
project.fileDeleted(fileName);
update(fileName);
update();
})
.on('change', (fileName) => {
project.fileUpdated(fileName);
update(fileName);
update();
});

function createWatcher(rootPath: string, extension: string[]) {
Expand All @@ -45,10 +45,7 @@ function createWatcher(rootPath: string, extension: string[]) {
});
}

async function update(fileNameCheckRelated?: string) {

if (fileNameCheckRelated && !project.isKnownRelatedFile(fileNameCheckRelated))
return;
async function update() {

const currentReq = ++req;
const isCanceled = () => currentReq !== req;
Expand Down
61 changes: 26 additions & 35 deletions packages/kit/src/createFormatter.ts
@@ -1,8 +1,7 @@
import { Config, FormattingOptions, LanguageServiceHost, createLanguageService } from '@volar/language-service';
import type * as ts from 'typescript/lib/tsserverlibrary';
import { Config, FormattingOptions, TypeScriptLanguageHost, createLanguageService } from '@volar/language-service';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { URI } from 'vscode-uri';
import { asPosix, defaultCompilerOptions, fileNameToUri, getConfiguration, uriToFileName } from './utils';
import { asPosix, defaultCompilerOptions, fileNameToUri, fs, getConfiguration, uriToFileName } from './utils';

export function createFormatter(
config: Config,
Expand All @@ -13,26 +12,27 @@ export function createFormatter(

let settings = {} as any;
let dummyScriptUri = 'file:///dummy.txt';
let dummyScriptFileName = '/dummy.txt';
let dummyScriptVersion = 0;
let dummyScriptSnapshot = ts.ScriptSnapshot.fromString('');
let dummyScriptLanguageId: string | undefined;
let fakeScriptVersion = 0;
let fakeScriptFileName = '/dummy.txt';
let fakeScriptSnapshot = ts.ScriptSnapshot.fromString('');
let fakeScriptLanguageId: string | undefined;

const service = createLanguageService(
{ typescript: ts },
{
rootUri: URI.file('/'),
uriToFileName: uri => {
if (uri.startsWith(dummyScriptUri))
return uri.replace(dummyScriptUri, dummyScriptFileName);
return uri.replace(dummyScriptUri, fakeScriptFileName);
return uriToFileName(uri);
},
fileNameToUri: fileName => {
if (fileName.startsWith(dummyScriptFileName))
return fileName.replace(dummyScriptFileName, dummyScriptUri);
if (fileName.startsWith(fakeScriptFileName))
return fileName.replace(fakeScriptFileName, dummyScriptUri);
return fileNameToUri(fileName);
},
getConfiguration: section => getConfiguration(settings, section),
fs,
},
config,
createHost(),
Expand Down Expand Up @@ -63,9 +63,9 @@ export function createFormatter(
}

async function formatCode(content: string, languageId: string, options: FormattingOptions): Promise<string> {
dummyScriptSnapshot = ts.ScriptSnapshot.fromString(content);
dummyScriptLanguageId = languageId;
dummyScriptVersion++;
fakeScriptSnapshot = ts.ScriptSnapshot.fromString(content);
fakeScriptVersion++;
fakeScriptLanguageId = languageId;
const document = service.context.getTextDocument(dummyScriptUri)!;
const edits = await service.format(dummyScriptUri, options, undefined, undefined);
if (edits?.length) {
Expand All @@ -76,35 +76,26 @@ export function createFormatter(
}

function createHost() {
const scriptVersions = new Map<string, number>();
const scriptSnapshots = new Map<string, ts.IScriptSnapshot>();
const host: LanguageServiceHost = {
...ts.sys,
let projectVersion = 0;
const host: TypeScriptLanguageHost = {
getCurrentDirectory: () => '/',
getCompilationSettings: () => compilerOptions,
getScriptFileNames: () => dummyScriptSnapshot ? [dummyScriptFileName] : [],
getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames,
getProjectVersion: () => projectVersion++,
getScriptFileNames: () => fakeScriptSnapshot ? [fakeScriptFileName] : [],
getScriptVersion: (fileName) => {
if (fileName === dummyScriptFileName) {
return dummyScriptVersion.toString();
if (fileName === fakeScriptFileName) {
return fakeScriptVersion.toString();
}
return scriptVersions.get(fileName)?.toString() ?? '';
},
getScriptSnapshot: (fileName) => {
if (fileName === dummyScriptFileName) {
return dummyScriptSnapshot;
if (fileName === fakeScriptFileName) {
return fakeScriptSnapshot;
}
if (!scriptSnapshots.has(fileName)) {
const fileText = ts.sys.readFile(fileName);
if (fileText !== undefined) {
scriptSnapshots.set(fileName, ts.ScriptSnapshot.fromString(fileText));
}
}
return scriptSnapshots.get(fileName);
},
getScriptLanguageId: uri => {
if (uri === dummyScriptFileName) return dummyScriptLanguageId;
return undefined;
getLanguageId: fileName => {
if (fileName === fakeScriptFileName) {
return fakeScriptLanguageId;
}
},
};
return host;
Expand Down
15 changes: 8 additions & 7 deletions packages/kit/src/createLinter.ts
@@ -1,10 +1,10 @@
import { CodeActionTriggerKind, Config, Diagnostic, DiagnosticSeverity, LanguageServiceHost, createLanguageService, mergeWorkspaceEdits } from '@volar/language-service';
import { CodeActionTriggerKind, Config, Diagnostic, DiagnosticSeverity, TypeScriptLanguageHost, createLanguageService, mergeWorkspaceEdits } from '@volar/language-service';
import type * as ts from 'typescript/lib/tsserverlibrary';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { asPosix, fileNameToUri, fs, getConfiguration, uriToFileName } from './utils';
import { URI } from 'vscode-uri';
import { asPosix, fileNameToUri, getConfiguration, uriToFileName } from './utils';

export function createLinter(config: Config, host: LanguageServiceHost) {
export function createLinter(config: Config, host: TypeScriptLanguageHost) {

let settings = {} as any;

Expand All @@ -14,8 +14,9 @@ export function createLinter(config: Config, host: LanguageServiceHost) {
{
uriToFileName,
fileNameToUri,
rootUri: URI.file(host.getCurrentDirectory()),
rootUri: URI.parse(fileNameToUri(host.getCurrentDirectory())),
getConfiguration: section => getConfiguration(settings, section),
fs,
},
config,
host,
Expand Down Expand Up @@ -101,9 +102,9 @@ export function createLinter(config: Config, host: LanguageServiceHost) {
messageText: diagnostic.message,
}));
const text = ts.formatDiagnosticsWithColorAndContext(errors, {
getCurrentDirectory: () => rootPath ?? host.getCurrentDirectory(),
getCanonicalFileName: (fileName) => host.useCaseSensitiveFileNames?.() ? fileName : fileName.toLowerCase(),
getNewLine: () => host.getNewLine?.() ?? '\n',
getCurrentDirectory: () => rootPath,
getCanonicalFileName: (fileName) => ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(),
getNewLine: () => ts.sys.newLine,
});
return text;
}
Expand Down
83 changes: 29 additions & 54 deletions packages/kit/src/createProject.ts
@@ -1,4 +1,4 @@
import { LanguageServiceHost } from '@volar/language-service';
import type { TypeScriptLanguageHost } from '@volar/language-service';
import * as path from 'typesafe-path/posix';
import type * as ts from 'typescript/lib/tsserverlibrary';
import { asPosix, defaultCompilerOptions } from './utils';
Expand Down Expand Up @@ -42,90 +42,74 @@ export function createProject(
);
}

function createProjectBase(
rootPath: string,
createParsedCommandLine: () => Pick<ts.ParsedCommandLine, 'options' | 'fileNames'>
) {
function createProjectBase(rootPath: string, createParsedCommandLine: () => Pick<ts.ParsedCommandLine, 'options' | 'fileNames'>) {

const ts = require('typescript') as typeof import('typescript/lib/tsserverlibrary');
const host: LanguageServiceHost = {
...ts.sys,
getCurrentDirectory: () => rootPath,
fileExists,
useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames,
getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
getCompilationSettings: () => parsedCommandLine.options,
const languageHost: TypeScriptLanguageHost = {
getCurrentDirectory: () => {
return rootPath;
},
getCompilationSettings: () => {
return parsedCommandLine.options;
},
getProjectVersion: () => {
checkRootFilesUpdate();
return projectVersion.toString();
},
getTypeRootsVersion: () => {
return typeRootsVersion;
return projectVersion;
},
getScriptFileNames: () => {
checkRootFilesUpdate();
return parsedCommandLine.fileNames;
},
getScriptVersion: (fileName) => scriptVersions[fileName]?.toString() ?? '',
getScriptVersion: (fileName) => {
return scriptVersions.get(fileName)?.toString();
},
getScriptSnapshot: (fileName) => {
const version = host.getScriptVersion(fileName);
if (!scriptSnapshots[fileName] || scriptSnapshots[fileName][0] !== version) {
if (!scriptSnapshotsCache.has(fileName)) {
const fileText = ts.sys.readFile(fileName, 'utf8');
if (fileText !== undefined) {
scriptSnapshots[fileName] = [version, ts.ScriptSnapshot.fromString(fileText)];
scriptSnapshotsCache.set(fileName, ts.ScriptSnapshot.fromString(fileText));
}
else {
scriptSnapshots[fileName] = [version, undefined];
scriptSnapshotsCache.set(fileName, undefined);
}
}
return scriptSnapshots[fileName][1];
return scriptSnapshotsCache.get(fileName);
},
};

let fileExistsCache: Record<string, boolean> = {};
let scriptVersions: Record<string, number> = {};
let scriptSnapshots: Record<string, [string, ts.IScriptSnapshot | undefined]> = {};
let scriptSnapshotsCache: Map<string, ts.IScriptSnapshot | undefined> = new Map();
let scriptVersions: Map<string, number> = new Map();
let parsedCommandLine = createParsedCommandLine();
let projectVersion = 0;
let typeRootsVersion = 0;
let shouldCheckRootFiles = false;

return {
languageServiceHost: host,
isKnownRelatedFile,
languageHost,
fileUpdated(fileName: string) {
scriptVersions.set(fileName, (scriptVersions.get(fileName) ?? 0) + 1);
fileName = asPosix(fileName);
if (isKnownRelatedFile(fileName)) {
if (scriptSnapshotsCache.has(fileName)) {
projectVersion++;
scriptVersions[fileName] ??= 0;
scriptVersions[fileName]++;
scriptSnapshotsCache.delete(fileName);
}
},
fileDeleted(fileName: string) {
scriptVersions.set(fileName, (scriptVersions.get(fileName) ?? 0) + 1);
fileName = asPosix(fileName);
fileExistsCache[fileName] = false;
if (isKnownRelatedFile(fileName)) {
if (scriptSnapshotsCache.has(fileName)) {
projectVersion++;
delete scriptVersions[fileName];
delete scriptSnapshots[fileName];
scriptSnapshotsCache.delete(fileName);
parsedCommandLine.fileNames = parsedCommandLine.fileNames.filter(name => name !== fileName);
}
},
fileCreated(fileName: string) {
scriptVersions.set(fileName, (scriptVersions.get(fileName) ?? 0) + 1);
fileName = asPosix(fileName);
if (isKnownRelatedFile(fileName)) {
projectVersion++;
typeRootsVersion++;
}
shouldCheckRootFiles = true;
fileExistsCache[fileName] = true;
scriptVersions[fileName] ??= 0;
scriptVersions[fileName]++;
},
reload() {
fileExistsCache = {};
scriptVersions = {};
scriptSnapshots = {};
scriptVersions.clear();
scriptSnapshotsCache.clear();
projectVersion++;
parsedCommandLine = createParsedCommandLine();
},
Expand All @@ -142,13 +126,4 @@ function createProjectBase(
projectVersion++;
}
}

function fileExists(fileName: string) {
fileExistsCache[fileName] ??= ts.sys.fileExists(fileName);
return fileExistsCache[fileName];
}

function isKnownRelatedFile(fileName: string) {
return scriptSnapshots[fileName] !== undefined || fileExistsCache[fileName] !== undefined;
}
}
37 changes: 37 additions & 0 deletions packages/kit/src/utils.ts
@@ -1,6 +1,8 @@
import * as path from 'typesafe-path/posix';
import { URI } from 'vscode-uri';
import type * as ts from 'typescript/lib/tsserverlibrary';
import { FileSystem, FileType } from '@volar/language-service';
import * as _fs from 'fs';

export const defaultCompilerOptions: ts.CompilerOptions = {
allowJs: true,
Expand Down Expand Up @@ -41,3 +43,38 @@ export function getConfiguration(settings: any, section: string) {
}
return result;
}

export const fs: FileSystem = {
readFile(uri, encoding) {
return _fs.readFileSync(uriToFileName(uri), { encoding: (encoding as 'utf8') ?? 'utf8' });
},
readDirectory(uri) {
if (uri.startsWith('file://')) {
const dirName = uriToFileName(uri);
const files = _fs.existsSync(dirName) ? _fs.readdirSync(dirName, { withFileTypes: true }) : [];
return files.map<[string, FileType]>(file => {
return [file.name, file.isFile() ? FileType.File
: file.isDirectory() ? FileType.Directory
: file.isSymbolicLink() ? FileType.SymbolicLink
: FileType.Unknown];
});
}
return [];
},
stat(uri) {
if (uri.startsWith('file://')) {
const stats = _fs.statSync(uriToFileName(uri), { throwIfNoEntry: false });
if (stats) {
return {
type: stats.isFile() ? FileType.File
: stats.isDirectory() ? FileType.Directory
: stats.isSymbolicLink() ? FileType.SymbolicLink
: FileType.Unknown,
ctime: stats.ctimeMs,
mtime: stats.mtimeMs,
size: stats.size,
};
}
}
},
};

0 comments on commit b0ac35c

Please sign in to comment.