initial scribbles

This commit is contained in:
Nathan Shively-Sanders
2022-03-01 09:56:48 -08:00
parent 71918750f9
commit da2622cc7b
6 changed files with 255 additions and 15 deletions
+62 -2
View File
@@ -1746,11 +1746,11 @@ namespace ts.server {
if (configFileExistenceInfo) {
const infoIsRootOfInferredProject = configFileExistenceInfo.openFilesImpactedByConfigFile?.get(info.path);
// Delete the info from map, since this file is no more open
// Delete the info from map, since this file is not open anymore
configFileExistenceInfo.openFilesImpactedByConfigFile?.delete(info.path);
// If the script info was not root of inferred project,
// there wont be config file watch open because of this script info
// there won't be config file watch open because of this script info
if (infoIsRootOfInferredProject) {
// But if it is a root, it could be the last script info that is root of inferred project
// and hence we would need to close the config file watcher
@@ -3685,6 +3685,66 @@ namespace ts.server {
return files;
}
/* @internal */
updateFileSystem(createdFiles: Iterator<protocol.FileSystemRequestArgs> | undefined, updatedFiles?: Iterator<protocol.FileSystemRequestArgs>, deletedFiles?: string[]) {
// TODO: Maybe it is somehow gauche or verboten to use protocol types but the translation in applyChangesInOpenFiles seems stupid
// It is also WEIRD that we use iterators here not arrays
// TODO: Probably copy what vfsUtil does to hook up a virtual filesystem here. Maybe.
// ugggggggggg have to copy over all that stuff to here
// (though I probably need to create a vfs project anyway, so why not)
const fs = new vfs.FileSystem(/*ignoreCase*/ true, {
files: {
[builtFolder]: new Mount(vpath.resolve(host.getWorkspaceRoot(), "built/local"), resolver),
[testLibFolder]: new Mount(vpath.resolve(host.getWorkspaceRoot(), "tests/lib"), resolver),
[projectsFolder]: new Mount(vpath.resolve(host.getWorkspaceRoot(), "tests/projects"), resolver),
[srcFolder]: {}
},
cwd: srcFolder,
meta: { defaultLibLocation: builtFolder }
})
if (!this.fs)
this.fs = fs
if (createdFiles) {
for (const document of Array.from(createdFiles)) {
fs.mkdirpSync(vpath.dirname(document.file));
fs.writeFileSync(document.file, document.text, "utf8");
fs.filemeta(document.file).set("document", document);
// Add symlinks
const symlink = document.meta.get("symlink");
if (symlink) {
for (const link of symlink.split(",").map(link => link.trim())) {
fs.mkdirpSync(vpath.dirname(link));
fs.symlinkSync(vpath.resolve(fs.cwd(), document.file), link);
}
}
}
}
// 1. set some internal tsserver state for mocked FS (if it hasn't already been set, this might not be the first message)
// - change this.host at least
this.host // no, it's readonly, we get this from a parent object
// - ScriptInfo instances might need to update their host -- what is textStorage?
// - they deffo have FileWatcher instances
// - TextStorage.host needs to update
// 2. create
// 3. update
// 4. delete
// - closeClientFile -> closeOpenFile -> ScriptInfo.close
// 5. ???
// 6. success!
if (createdFiles) {
}
if (updatedFiles) {
}
if (deletedFiles) {
for (const file of deletedFiles) {
this.closeClientFile(file) // maybe copy stuff from applyChanges -- mostly I just want to leave a pointer to more code
}
}
}
/* @internal */
applyChangesInOpenFiles(openFiles: Iterator<OpenFileArguments> | undefined, changedFiles?: Iterator<ChangeFileArguments>, closedFiles?: string[]): void {
let openScriptInfos: ScriptInfo[] | undefined;
+34 -2
View File
@@ -96,6 +96,7 @@ namespace ts.server.protocol {
/* @internal */
ApplyChangedToOpenFiles = "applyChangedToOpenFiles",
UpdateOpen = "updateOpen",
UpdateFileSystem = "updateFileSystem",
/* @internal */
EncodedSyntacticClassificationsFull = "encodedSyntacticClassifications-full",
/* @internal */
@@ -154,7 +155,7 @@ namespace ts.server.protocol {
PrepareCallHierarchy = "prepareCallHierarchy",
ProvideCallHierarchyIncomingCalls = "provideCallHierarchyIncomingCalls",
ProvideCallHierarchyOutgoingCalls = "provideCallHierarchyOutgoingCalls",
ProvideInlayHints = "provideInlayHints"
ProvideInlayHints = "provideInlayHints",
// NOTE: If updating this, be sure to also update `allCommandNames` in `testRunner/unittests/tsserver/session.ts`.
}
@@ -1805,6 +1806,7 @@ namespace ts.server.protocol {
/**
* Request to synchronize list of open files with the client
* TODO: Lots of unit tests refer to this too, a good starting point for UpdateFileSystemRequest
*/
export interface UpdateOpenRequest extends Request {
command: CommandTypes.UpdateOpen;
@@ -1820,7 +1822,7 @@ namespace ts.server.protocol {
*/
openFiles?: OpenRequestArgs[];
/**
* List of open files files that were changes
* List of open files files that were changed
*/
changedFiles?: FileCodeEdits[];
/**
@@ -1829,6 +1831,36 @@ namespace ts.server.protocol {
closedFiles?: string[];
}
export interface UpdateFileSystemRequest extends Request {
command: CommandTypes.UpdateFileSystem;
arguments: UpdateFileSystemRequestArgs;
}
export interface UpdateFileSystemRequestArgs {
/** For now, only 'memfs', initially for exclusive in-memory operation, but it could be other in-memory names later */
fileSystem: string;
/** For now, a list of newly created or newly available files. Probably need to ADD mocked file watchers */
created: FileSystemRequestArgs[];
/** Just-deleted files. Also needs to trigger and then remove file watchers (I think) */
deleted: string[];
/** Needs to replace what file watchers would normally listen to */
updated: FileSystemRequestArgs[];
}
export interface FileSystemRequestArgs extends FileRequestArgs {
/**
* Used to replace the content that would be on disk.
* Then the known content will be used upon opening instead of the disk copy
*/
fileContent?: string;
/**
* Used to specify the script kind of the file explicitly. It could be one of the following:
* "TS", "JS", "TSX", "JSX"
* TODO: Not 100% sure this is needed.
*/
scriptKindName?: ScriptKindName;
}
/**
* External projects have a typeAcquisition option so they need to be added separately to compiler options for inferred projects.
*/
+20 -9
View File
@@ -714,6 +714,7 @@ namespace ts.server {
export class Session<TMessage = string> implements EventSender {
private readonly gcTimer: GcTimer;
// TODO: This will need to be replaced too
protected projectService: ProjectService;
private changeSeq = 0;
@@ -722,6 +723,7 @@ namespace ts.server {
private currentRequestId!: number;
private errorCheck: MultistepOperation;
// TODO: Replace this one?
protected host: ServerHost;
private readonly cancellationToken: ServerCancellationToken;
protected readonly typingsInstaller: ITypingsInstaller;
@@ -2643,16 +2645,16 @@ namespace ts.server {
[CommandNames.UpdateOpen]: (request: protocol.UpdateOpenRequest) => {
this.changeSeq++;
this.projectService.applyChangesInOpenFiles(
request.arguments.openFiles && mapIterator(arrayIterator(request.arguments.openFiles), file => ({
fileName: file.file,
content: file.fileContent,
scriptKind: file.scriptKindName,
projectRootPath: file.projectRootPath
request.arguments.openFiles && mapIterator(arrayIterator(request.arguments.openFiles), ({ file, fileContent, scriptKindName, projectRootPath }) => ({
fileName: file,
content: fileContent,
scriptKind: scriptKindName,
projectRootPath
})),
request.arguments.changedFiles && mapIterator(arrayIterator(request.arguments.changedFiles), file => ({
fileName: file.fileName,
changes: mapDefinedIterator(arrayReverseIterator(file.textChanges), change => {
const scriptInfo = Debug.checkDefined(this.projectService.getScriptInfo(file.fileName));
request.arguments.changedFiles && mapIterator(arrayIterator(request.arguments.changedFiles), ({ fileName, textChanges}) => ({
fileName,
changes: mapDefinedIterator(arrayReverseIterator(textChanges), change => {
const scriptInfo = Debug.checkDefined(this.projectService.getScriptInfo(fileName));
const start = scriptInfo.lineOffsetToPosition(change.start.line, change.start.offset);
const end = scriptInfo.lineOffsetToPosition(change.end.line, change.end.offset);
return start >= 0 ? { span: { start, length: end - start }, newText: change.newText } : undefined;
@@ -2676,6 +2678,15 @@ namespace ts.server {
// TODO: report errors
return this.requiredResponse(/*response*/ true);
},
[CommandNames.UpdateFileSystem]: (request: protocol.UpdateFileSystemRequest) => {
this.changeSeq++;
this.projectService.updateFileSystem(
request.arguments.created && arrayIterator(request.arguments.created), // open
request.arguments.updated && arrayIterator(request.arguments.updated), //change
request.arguments.deleted, // close
);
return this.requiredResponse(/*response*/ true)
},
[CommandNames.Exit]: () => {
this.exit();
return this.notRequired();
+2 -1
View File
@@ -11,7 +11,8 @@
"references": [
{ "path": "../compiler" },
{ "path": "../jsTyping" },
{ "path": "../services" }
{ "path": "../services" },
{ "path": "../vfs" }
],
"files": [
"types.ts",
@@ -1,4 +1,139 @@
namespace ts.projectSystem {
// TODO: Make a separate file at some point
describe("unittests:: tsserver:: updateFileSystem", () => {
interface Verify {
applyChangesToOpen: (session: TestSession) => void;
openFile1Again: (session: TestSession) => void;
}
const configFile: protocol.FileSystemRequestArgs = {
file: "/a/b/tsconfig.json",
fileContent: "{}"
};
const file3: protocol.FileSystemRequestArgs = {
file: "/a/b/file3.ts",
fileContent: "export let xyz = 1;"
};
const app: protocol.FileSystemRequestArgs = {
file: "/a/b/app.ts",
fileContent: "import { xyz } from './file3'; let x = xyz"
};
function fileContentWithComment(file: protocol.FileSystemRequestArgs | File) {
return `// some copy right notice
${'content' in file ? file.content : file.fileContent}`;
}
function verify({ applyChangesToOpen, openFile1Again }: Verify) {
// TODO: Replace with createMemfsServerHost
const host = createServerHost([commonFile1, commonFile2, libFile]);
const session = createSession(host);
session.executeCommandSeq<protocol.UpdateFileSystemRequest>({
command: protocol.CommandTypes.UpdateFileSystem,
arguments:{
fileSystem: 'memfs',
created: [configFile, file3, app],
deleted: [], // string[];
updated: [], //FileSystemRequestArgs[];
}
});
const service = session.getProjectService();
// session.host
const project = service.configuredProjects.get(configFile.file)!;
assert.isDefined(project);
verifyProjectVersion(project, 1);
session.executeCommandSeq<protocol.OpenRequest>({
command: protocol.CommandTypes.Open,
arguments: app
});
verifyProjectVersion(project, 2);
// TODO: Verify file watchers have updated
// Verify Texts
verifyText(service, commonFile1.path, commonFile1.content);
verifyText(service, commonFile2.path, commonFile2.content);
verifyText(service, app.file, app.fileContent!);
verifyText(service, file3.file, fileContentWithComment(file3));
// Apply changes
applyChangesToOpen(session);
// Verify again
verifyProjectVersion(project, 3);
// Open file contents
verifyText(service, commonFile1.path, fileContentWithComment(commonFile1));
verifyText(service, commonFile2.path, fileContentWithComment(commonFile2));
verifyText(service, app.file, "let zzz = 10;let zz = 10;let z = 1;");
verifyText(service, file3.file, file3.fileContent!);
// Open file1 again
openFile1Again(session);
assert.isTrue(service.getScriptInfo(commonFile1.path)!.isScriptOpen());
// Verify that file1 contents are changed
verifyProjectVersion(project, 4);
verifyText(service, commonFile1.path, commonFile1.content);
verifyText(service, commonFile2.path, fileContentWithComment(commonFile2));
verifyText(service, app.file, "let zzz = 10;let zz = 10;let z = 1;");
verifyText(service, file3.file, file3.fileContent!);
}
function verifyText(service: server.ProjectService, file: string, expected: string) {
const info = service.getScriptInfo(file)!;
const snap = info.getSnapshot();
// Verified applied in reverse order
assert.equal(snap.getText(0, snap.getLength()), expected, `Text of changed file: ${file}`);
}
function verifyProjectVersion(project: server.Project, expected: number) {
assert.equal(Number(project.getProjectVersion()), expected);
}
it("with updateOpen request", () => {
verify({
applyChangesToOpen: session => session.executeCommandSeq<protocol.UpdateOpenRequest>({
command: protocol.CommandTypes.UpdateOpen,
arguments: {
openFiles: [
{
file: commonFile1.path,
fileContent: fileContentWithComment(commonFile1)
},
{
file: commonFile2.path,
fileContent: fileContentWithComment(commonFile2)
}
],
changedFiles: [
{
fileName: app.file,
textChanges: [
{
start: { line: 1, offset: 1 },
end: { line: 1, offset: 1 },
newText: "let zzz = 10;",
},
{
start: { line: 1, offset: 1 },
end: { line: 1, offset: 1 },
newText: "let zz = 10;",
}
]
}
],
closedFiles: [
file3.file
]
}
}),
openFile1Again: session => session.executeCommandSeq<protocol.UpdateOpenRequest>({
command: protocol.CommandTypes.UpdateOpen,
arguments: {
openFiles: [{
file: commonFile1.path,
fileContent: commonFile1.content
}]
}
}),
});
});
})
describe("unittests:: tsserver:: applyChangesToOpenFiles", () => {
const configFile: File = {
path: "/a/b/tsconfig.json",
+2 -1
View File
@@ -278,7 +278,8 @@ namespace ts.server {
CommandNames.ToggleMultilineComment,
CommandNames.CommentSelection,
CommandNames.UncommentSelection,
CommandNames.ProvideInlayHints
CommandNames.ProvideInlayHints,
CommandNames.UpdateFileSystem,
];
it("should not throw when commands are executed with invalid arguments", () => {