feat(language-service): [ivy] wrap ngtsc to handle typecheck files (#36930)
This commit adds a Compiler interface that wraps the actual ngtsc compiler. The language-service specific compiler manages multiple typecheck files using the Project interface, creating and adding ScriptInfos as necessary. This commit also adds `overrideInlineTemplate()` method to the mock service so that we could test the Compiler diagnostics feature. PR Close #36930
This commit is contained in:
parent
82a3bd5e8b
commit
1142c378fd
@ -1,12 +1,13 @@
|
|||||||
load("//tools:defaults.bzl", "ts_library")
|
load("//tools:defaults.bzl", "ts_library")
|
||||||
|
|
||||||
package(default_visibility = ["//visibility:public"])
|
package(default_visibility = ["//packages/language-service:__subpackages__"])
|
||||||
|
|
||||||
ts_library(
|
ts_library(
|
||||||
name = "ivy",
|
name = "ivy",
|
||||||
srcs = glob(["*.ts"]),
|
srcs = glob(["*.ts"]),
|
||||||
deps = [
|
deps = [
|
||||||
"//packages/compiler-cli",
|
"//packages/compiler-cli",
|
||||||
|
"//packages/language-service/ivy/compiler",
|
||||||
"@npm//typescript",
|
"@npm//typescript",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
15
packages/language-service/ivy/compiler/BUILD.bazel
Normal file
15
packages/language-service/ivy/compiler/BUILD.bazel
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
load("//tools:defaults.bzl", "ts_library")
|
||||||
|
|
||||||
|
package(default_visibility = ["//packages/language-service/ivy:__pkg__"])
|
||||||
|
|
||||||
|
ts_library(
|
||||||
|
name = "compiler",
|
||||||
|
srcs = glob(["*.ts"]),
|
||||||
|
deps = [
|
||||||
|
"//packages/compiler-cli",
|
||||||
|
"//packages/compiler-cli/src/ngtsc/core",
|
||||||
|
"//packages/compiler-cli/src/ngtsc/file_system",
|
||||||
|
"//packages/compiler-cli/src/ngtsc/typecheck",
|
||||||
|
"@npm//typescript",
|
||||||
|
],
|
||||||
|
)
|
2
packages/language-service/ivy/compiler/README.md
Normal file
2
packages/language-service/ivy/compiler/README.md
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
All files in this directory are temporary. This is created to simulate the final
|
||||||
|
form of the Ivy compiler that supports language service.
|
120
packages/language-service/ivy/compiler/compiler.ts
Normal file
120
packages/language-service/ivy/compiler/compiler.ts
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {CompilerOptions} from '@angular/compiler-cli';
|
||||||
|
import {NgCompiler, NgCompilerHost} from '@angular/compiler-cli/src/ngtsc/core';
|
||||||
|
import {AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system';
|
||||||
|
import {TypeCheckingProgramStrategy, UpdateMode} from '@angular/compiler-cli/src/ngtsc/typecheck';
|
||||||
|
import * as ts from 'typescript/lib/tsserverlibrary';
|
||||||
|
|
||||||
|
import {makeCompilerHostFromProject} from './compiler_host';
|
||||||
|
|
||||||
|
export class Compiler {
|
||||||
|
private tsCompilerHost: ts.CompilerHost;
|
||||||
|
private lastKnownProgram: ts.Program;
|
||||||
|
private compiler: NgCompiler;
|
||||||
|
private readonly strategy: TypeCheckingProgramStrategy;
|
||||||
|
|
||||||
|
constructor(private readonly project: ts.server.Project, private options: CompilerOptions) {
|
||||||
|
this.tsCompilerHost = makeCompilerHostFromProject(project);
|
||||||
|
const ngCompilerHost = NgCompilerHost.wrap(
|
||||||
|
this.tsCompilerHost,
|
||||||
|
project.getRootFiles(), // input files
|
||||||
|
options,
|
||||||
|
null, // old program
|
||||||
|
);
|
||||||
|
this.strategy = createTypeCheckingProgramStrategy(project);
|
||||||
|
this.lastKnownProgram = this.strategy.getProgram();
|
||||||
|
this.compiler = new NgCompiler(ngCompilerHost, options, this.lastKnownProgram, this.strategy);
|
||||||
|
}
|
||||||
|
|
||||||
|
setCompilerOptions(options: CompilerOptions) {
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
analyze(): ts.Program|undefined {
|
||||||
|
const inputFiles = this.project.getRootFiles();
|
||||||
|
const ngCompilerHost =
|
||||||
|
NgCompilerHost.wrap(this.tsCompilerHost, inputFiles, this.options, this.lastKnownProgram);
|
||||||
|
const program = this.strategy.getProgram();
|
||||||
|
this.compiler =
|
||||||
|
new NgCompiler(ngCompilerHost, this.options, program, this.strategy, this.lastKnownProgram);
|
||||||
|
try {
|
||||||
|
// This is the only way to force the compiler to update the typecheck file
|
||||||
|
// in the program. We have to do try-catch because the compiler immediately
|
||||||
|
// throws if it fails to parse any template in the entire program!
|
||||||
|
const d = this.compiler.getDiagnostics();
|
||||||
|
if (d.length) {
|
||||||
|
// There could be global compilation errors. It's useful to print them
|
||||||
|
// out in development.
|
||||||
|
console.error(d.map(d => ts.flattenDiagnosticMessageText(d.messageText, '\n')));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to analyze program', e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.lastKnownProgram = this.compiler.getNextProgram();
|
||||||
|
return this.lastKnownProgram;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDiagnostics(sourceFile: ts.SourceFile): ts.Diagnostic[] {
|
||||||
|
return this.compiler.getDiagnostics(sourceFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTypeCheckingProgramStrategy(project: ts.server.Project):
|
||||||
|
TypeCheckingProgramStrategy {
|
||||||
|
return {
|
||||||
|
getProgram(): ts.Program {
|
||||||
|
const program = project.getLanguageService().getProgram();
|
||||||
|
if (!program) {
|
||||||
|
throw new Error('Language service does not have a program!');
|
||||||
|
}
|
||||||
|
return program;
|
||||||
|
},
|
||||||
|
updateFiles(contents: Map<AbsoluteFsPath, string>, updateMode: UpdateMode) {
|
||||||
|
if (updateMode !== UpdateMode.Complete) {
|
||||||
|
throw new Error(`Incremental update mode is currently not supported`);
|
||||||
|
}
|
||||||
|
for (const [fileName, newText] of contents) {
|
||||||
|
const scriptInfo = getOrCreateTypeCheckScriptInfo(project, fileName);
|
||||||
|
const snapshot = scriptInfo.getSnapshot();
|
||||||
|
const length = snapshot.getLength();
|
||||||
|
scriptInfo.editContent(0, length, newText);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOrCreateTypeCheckScriptInfo(
|
||||||
|
project: ts.server.Project, tcf: string): ts.server.ScriptInfo {
|
||||||
|
// First check if there is already a ScriptInfo for the tcf
|
||||||
|
const {projectService} = project;
|
||||||
|
let scriptInfo = projectService.getScriptInfo(tcf);
|
||||||
|
if (!scriptInfo) {
|
||||||
|
// ScriptInfo needs to be opened by client to be able to set its user-defined
|
||||||
|
// content. We must also provide file content, otherwise the service will
|
||||||
|
// attempt to fetch the content from disk and fail.
|
||||||
|
scriptInfo = projectService.getOrCreateScriptInfoForNormalizedPath(
|
||||||
|
ts.server.toNormalizedPath(tcf),
|
||||||
|
true, // openedByClient
|
||||||
|
'', // fileContent
|
||||||
|
ts.ScriptKind.TS, // scriptKind
|
||||||
|
);
|
||||||
|
if (!scriptInfo) {
|
||||||
|
throw new Error(`Failed to create script info for ${tcf}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add ScriptInfo to project if it's missing. A ScriptInfo needs to be part of
|
||||||
|
// the project so that it becomes part of the program.
|
||||||
|
if (!project.containsScriptInfo(scriptInfo)) {
|
||||||
|
project.addRoot(scriptInfo);
|
||||||
|
}
|
||||||
|
return scriptInfo;
|
||||||
|
}
|
103
packages/language-service/ivy/compiler/compiler_host.ts
Normal file
103
packages/language-service/ivy/compiler/compiler_host.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as ts from 'typescript/lib/tsserverlibrary';
|
||||||
|
|
||||||
|
export function makeCompilerHostFromProject(project: ts.server.Project): ts.CompilerHost {
|
||||||
|
const compilerHost: ts.CompilerHost = {
|
||||||
|
fileExists(fileName: string): boolean {
|
||||||
|
return project.fileExists(fileName);
|
||||||
|
},
|
||||||
|
readFile(fileName: string): string |
|
||||||
|
undefined {
|
||||||
|
return project.readFile(fileName);
|
||||||
|
},
|
||||||
|
directoryExists(directoryName: string): boolean {
|
||||||
|
return project.directoryExists(directoryName);
|
||||||
|
},
|
||||||
|
getCurrentDirectory(): string {
|
||||||
|
return project.getCurrentDirectory();
|
||||||
|
},
|
||||||
|
getDirectories(path: string): string[] {
|
||||||
|
return project.getDirectories(path);
|
||||||
|
},
|
||||||
|
getSourceFile(
|
||||||
|
fileName: string, languageVersion: ts.ScriptTarget, onError?: (message: string) => void,
|
||||||
|
shouldCreateNewSourceFile?: boolean): ts.SourceFile |
|
||||||
|
undefined {
|
||||||
|
const path = project.projectService.toPath(fileName);
|
||||||
|
return project.getSourceFile(path);
|
||||||
|
},
|
||||||
|
getSourceFileByPath(
|
||||||
|
fileName: string, path: ts.Path, languageVersion: ts.ScriptTarget,
|
||||||
|
onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): ts.SourceFile |
|
||||||
|
undefined {
|
||||||
|
return project.getSourceFile(path);
|
||||||
|
},
|
||||||
|
getCancellationToken(): ts.CancellationToken {
|
||||||
|
return {
|
||||||
|
isCancellationRequested() {
|
||||||
|
return project.getCancellationToken().isCancellationRequested();
|
||||||
|
},
|
||||||
|
throwIfCancellationRequested() {
|
||||||
|
if (this.isCancellationRequested()) {
|
||||||
|
throw new ts.OperationCanceledException();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getDefaultLibFileName(options: ts.CompilerOptions): string {
|
||||||
|
return project.getDefaultLibFileName();
|
||||||
|
},
|
||||||
|
writeFile(
|
||||||
|
fileName: string, data: string, writeByteOrderMark: boolean,
|
||||||
|
onError?: (message: string) => void, sourceFiles?: readonly ts.SourceFile[]) {
|
||||||
|
return project.writeFile(fileName, data);
|
||||||
|
},
|
||||||
|
getCanonicalFileName(fileName: string): string {
|
||||||
|
return project.projectService.toCanonicalFileName(fileName);
|
||||||
|
},
|
||||||
|
useCaseSensitiveFileNames(): boolean {
|
||||||
|
return project.useCaseSensitiveFileNames();
|
||||||
|
},
|
||||||
|
getNewLine(): string {
|
||||||
|
return project.getNewLine();
|
||||||
|
},
|
||||||
|
readDirectory(
|
||||||
|
rootDir: string, extensions: readonly string[], excludes: readonly string[]|undefined,
|
||||||
|
includes: readonly string[], depth?: number): string[] {
|
||||||
|
return project.readDirectory(rootDir, extensions, excludes, includes, depth);
|
||||||
|
},
|
||||||
|
resolveModuleNames(
|
||||||
|
moduleNames: string[], containingFile: string, reusedNames: string[]|undefined,
|
||||||
|
redirectedReference: ts.ResolvedProjectReference|undefined, options: ts.CompilerOptions):
|
||||||
|
(ts.ResolvedModule | undefined)[] {
|
||||||
|
return project.resolveModuleNames(
|
||||||
|
moduleNames, containingFile, reusedNames, redirectedReference);
|
||||||
|
},
|
||||||
|
resolveTypeReferenceDirectives(
|
||||||
|
typeReferenceDirectiveNames: string[], containingFile: string,
|
||||||
|
redirectedReference: ts.ResolvedProjectReference|undefined, options: ts.CompilerOptions):
|
||||||
|
(ts.ResolvedTypeReferenceDirective | undefined)[] {
|
||||||
|
return project.resolveTypeReferenceDirectives(
|
||||||
|
typeReferenceDirectiveNames, containingFile, redirectedReference);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (project.trace) {
|
||||||
|
compilerHost.trace = function trace(s: string) {
|
||||||
|
project.trace!(s);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (project.realpath) {
|
||||||
|
compilerHost.realpath = function realpath(path: string): string {
|
||||||
|
return project.realpath!(path);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return compilerHost;
|
||||||
|
}
|
@ -8,18 +8,29 @@
|
|||||||
|
|
||||||
import {CompilerOptions, createNgCompilerOptions} from '@angular/compiler-cli';
|
import {CompilerOptions, createNgCompilerOptions} from '@angular/compiler-cli';
|
||||||
import * as ts from 'typescript/lib/tsserverlibrary';
|
import * as ts from 'typescript/lib/tsserverlibrary';
|
||||||
|
import {Compiler} from './compiler/compiler';
|
||||||
|
|
||||||
export class LanguageService {
|
export class LanguageService {
|
||||||
private options: CompilerOptions;
|
private options: CompilerOptions;
|
||||||
|
private readonly compiler: Compiler;
|
||||||
|
|
||||||
constructor(project: ts.server.Project, private readonly tsLS: ts.LanguageService) {
|
constructor(project: ts.server.Project, private readonly tsLS: ts.LanguageService) {
|
||||||
this.options = parseNgCompilerOptions(project);
|
this.options = parseNgCompilerOptions(project);
|
||||||
this.watchConfigFile(project);
|
this.watchConfigFile(project);
|
||||||
|
this.compiler = new Compiler(project, this.options);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSemanticDiagnostics(fileName: string): ts.Diagnostic[] {
|
getSemanticDiagnostics(fileName: string): ts.Diagnostic[] {
|
||||||
|
const program = this.compiler.analyze();
|
||||||
|
if (!program) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
const sourceFile = program.getSourceFile(fileName);
|
||||||
|
if (!sourceFile) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return this.compiler.getDiagnostics(sourceFile);
|
||||||
|
}
|
||||||
|
|
||||||
private watchConfigFile(project: ts.server.Project) {
|
private watchConfigFile(project: ts.server.Project) {
|
||||||
// TODO: Check the case when the project is disposed. An InferredProject
|
// TODO: Check the case when the project is disposed. An InferredProject
|
||||||
@ -35,6 +46,7 @@ export class LanguageService {
|
|||||||
project.log(`Config file changed: ${fileName}`);
|
project.log(`Config file changed: ${fileName}`);
|
||||||
if (eventKind === ts.FileWatcherEventKind.Changed) {
|
if (eventKind === ts.FileWatcherEventKind.Changed) {
|
||||||
this.options = parseNgCompilerOptions(project);
|
this.options = parseNgCompilerOptions(project);
|
||||||
|
this.compiler.setCompilerOptions(this.options);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,9 @@ jasmine_node_test(
|
|||||||
"//packages/forms",
|
"//packages/forms",
|
||||||
"//packages/language-service/test:project",
|
"//packages/language-service/test:project",
|
||||||
],
|
],
|
||||||
|
tags = [
|
||||||
|
"ivy-only",
|
||||||
|
],
|
||||||
deps = [
|
deps = [
|
||||||
":test_lib",
|
":test_lib",
|
||||||
],
|
],
|
||||||
|
38
packages/language-service/ivy/test/diagnostic_spec.ts
Normal file
38
packages/language-service/ivy/test/diagnostic_spec.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as ts from 'typescript/lib/tsserverlibrary';
|
||||||
|
|
||||||
|
import {LanguageService} from '../language_service';
|
||||||
|
|
||||||
|
import {APP_COMPONENT, setup} from './mock_host';
|
||||||
|
|
||||||
|
describe('diagnostic', () => {
|
||||||
|
const {project, service, tsLS} = setup();
|
||||||
|
const ngLS = new LanguageService(project, tsLS);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not produce error for AppComponent', () => {
|
||||||
|
const diags = ngLS.getSemanticDiagnostics(APP_COMPONENT);
|
||||||
|
expect(diags).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should report member does not exist', () => {
|
||||||
|
const content = service.overwriteInlineTemplate(APP_COMPONENT, '{{ nope }}');
|
||||||
|
const diags = ngLS.getSemanticDiagnostics(APP_COMPONENT);
|
||||||
|
expect(diags.length).toBe(1);
|
||||||
|
const {category, file, start, length, messageText} = diags[0];
|
||||||
|
expect(category).toBe(ts.DiagnosticCategory.Error);
|
||||||
|
expect(file?.fileName).toBe(APP_COMPONENT);
|
||||||
|
expect(content.substring(start!, start! + length!)).toBe('nope');
|
||||||
|
expect(messageText).toBe(`Property 'nope' does not exist on type 'AppComponent'.`);
|
||||||
|
});
|
||||||
|
});
|
@ -35,6 +35,7 @@ export const TSCONFIG = join(PROJECT_DIR, 'tsconfig.json');
|
|||||||
export const APP_COMPONENT = join(PROJECT_DIR, 'app', 'app.component.ts');
|
export const APP_COMPONENT = join(PROJECT_DIR, 'app', 'app.component.ts');
|
||||||
export const APP_MAIN = join(PROJECT_DIR, 'app', 'main.ts');
|
export const APP_MAIN = join(PROJECT_DIR, 'app', 'main.ts');
|
||||||
export const PARSING_CASES = join(PROJECT_DIR, 'app', 'parsing-cases.ts');
|
export const PARSING_CASES = join(PROJECT_DIR, 'app', 'parsing-cases.ts');
|
||||||
|
export const TEST_TEMPLATE = join(PROJECT_DIR, 'app', 'test.ng');
|
||||||
|
|
||||||
const NOOP_FILE_WATCHER: ts.FileWatcher = {
|
const NOOP_FILE_WATCHER: ts.FileWatcher = {
|
||||||
close() {}
|
close() {}
|
||||||
@ -44,9 +45,14 @@ export const host: ts.server.ServerHost = {
|
|||||||
...ts.sys,
|
...ts.sys,
|
||||||
readFile(absPath: string, encoding?: string): string |
|
readFile(absPath: string, encoding?: string): string |
|
||||||
undefined {
|
undefined {
|
||||||
// TODO: Need to remove all annotations in templates like we do in
|
const content = ts.sys.readFile(absPath, encoding);
|
||||||
// MockTypescriptHost
|
if (content === undefined) {
|
||||||
return ts.sys.readFile(absPath, encoding);
|
return undefined;
|
||||||
|
}
|
||||||
|
if (absPath === APP_COMPONENT || absPath === PARSING_CASES || absPath === TEST_TEMPLATE) {
|
||||||
|
return removeReferenceMarkers(removeLocationMarkers(content));
|
||||||
|
}
|
||||||
|
return content;
|
||||||
},
|
},
|
||||||
watchFile(path: string, callback: ts.FileWatcherCallback): ts.FileWatcher {
|
watchFile(path: string, callback: ts.FileWatcherCallback): ts.FileWatcher {
|
||||||
return NOOP_FILE_WATCHER;
|
return NOOP_FILE_WATCHER;
|
||||||
@ -109,16 +115,20 @@ class MockService {
|
|||||||
|
|
||||||
overwrite(fileName: string, newText: string): string {
|
overwrite(fileName: string, newText: string): string {
|
||||||
const scriptInfo = this.getScriptInfo(fileName);
|
const scriptInfo = this.getScriptInfo(fileName);
|
||||||
this.overwritten.add(scriptInfo.fileName);
|
this.overwriteScriptInfo(scriptInfo, preprocess(newText));
|
||||||
const snapshot = scriptInfo.getSnapshot();
|
|
||||||
scriptInfo.editContent(0, snapshot.getLength(), preprocess(newText));
|
|
||||||
const sameProgram = this.project.updateGraph(); // clear the dirty flag
|
|
||||||
if (sameProgram) {
|
|
||||||
throw new Error('Project should have updated program after overwrite');
|
|
||||||
}
|
|
||||||
return newText;
|
return newText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
overwriteInlineTemplate(fileName: string, newTemplate: string): string {
|
||||||
|
const scriptInfo = this.getScriptInfo(fileName);
|
||||||
|
const snapshot = scriptInfo.getSnapshot();
|
||||||
|
const originalContent = snapshot.getText(0, snapshot.getLength());
|
||||||
|
const newContent =
|
||||||
|
originalContent.replace(/template: `([\s\S]+)`/, `template: \`${newTemplate}\``);
|
||||||
|
this.overwriteScriptInfo(scriptInfo, preprocess(newContent));
|
||||||
|
return newContent;
|
||||||
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
if (this.overwritten.size === 0) {
|
if (this.overwritten.size === 0) {
|
||||||
return;
|
return;
|
||||||
@ -130,10 +140,6 @@ class MockService {
|
|||||||
throw new Error(`Failed to reload ${scriptInfo.fileName}`);
|
throw new Error(`Failed to reload ${scriptInfo.fileName}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const sameProgram = this.project.updateGraph();
|
|
||||||
if (sameProgram) {
|
|
||||||
throw new Error('Project should have updated program after reset');
|
|
||||||
}
|
|
||||||
this.overwritten.clear();
|
this.overwritten.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,9 +150,26 @@ class MockService {
|
|||||||
}
|
}
|
||||||
return scriptInfo;
|
return scriptInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private overwriteScriptInfo(scriptInfo: ts.server.ScriptInfo, newText: string) {
|
||||||
|
const snapshot = scriptInfo.getSnapshot();
|
||||||
|
scriptInfo.editContent(0, snapshot.getLength(), newText);
|
||||||
|
this.overwritten.add(scriptInfo.fileName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const REGEX_CURSOR = /¦/g;
|
const REGEX_CURSOR = /¦/g;
|
||||||
function preprocess(text: string): string {
|
function preprocess(text: string): string {
|
||||||
return text.replace(REGEX_CURSOR, '');
|
return text.replace(REGEX_CURSOR, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const REF_MARKER = /«(((\w|\-)+)|([^ᐱ]*ᐱ(\w+)ᐱ.[^»]*))»/g;
|
||||||
|
const LOC_MARKER = /\~\{(\w+(-\w+)*)\}/g;
|
||||||
|
|
||||||
|
function removeReferenceMarkers(value: string): string {
|
||||||
|
return value.replace(REF_MARKER, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeLocationMarkers(value: string): string {
|
||||||
|
return value.replace(LOC_MARKER, '');
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user