From 12f5488140b4f875c178f6873028fcf1b7959bd0 Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 27 May 2020 07:44:14 +0200 Subject: [PATCH] fix(mapping): complete mappings refactoring --- src/configuration/configuration.service.ts | 159 +++--------------- src/configuration/mapping.service.ts | 34 ++++ src/configuration/tsconfig.service.ts | 85 ++++++++++ src/extension.ts | 10 +- .../javascript/javascript.provider.ts | 8 +- src/test/suite/configuration.service.test.ts | 47 +++--- 6 files changed, 173 insertions(+), 170 deletions(-) create mode 100644 src/configuration/mapping.service.ts create mode 100644 src/configuration/tsconfig.service.ts diff --git a/src/configuration/configuration.service.ts b/src/configuration/configuration.service.ts index 0573311..4d88989 100644 --- a/src/configuration/configuration.service.ts +++ b/src/configuration/configuration.service.ts @@ -1,103 +1,19 @@ import * as vscode from "vscode"; -import * as JSON5 from "json5"; -import { readFileSync } from "fs"; import { Config, Mapping } from "./configuration.interface"; +import { getWorkfolderTsConfigConfiguration } from "./tsconfig.service"; +import { parseMappings, replaceWorkspaceRoot } from "./mapping.service"; -let cachedConfiguration = new Map(); +export async function getConfiguration( + resource: vscode.Uri +): Promise> { + const workspaceFolder = vscode.workspace.getWorkspaceFolder(resource); -export async function subscribeToConfigurationService(): Promise< - vscode.Disposable[] -> { - await preFetchConfiguration(); + const getConfig = (key: string) => + vscode.workspace.getConfiguration(key, resource); - const disposables: vscode.Disposable[] = []; - for (const workfolder of vscode.workspace.workspaceFolders || []) { - const pattern = new vscode.RelativePattern(workfolder, "[tj]sconfig.json"); - const fileWatcher = vscode.workspace.createFileSystemWatcher(pattern); - fileWatcher.onDidChange(preFetchConfiguration); - disposables.push(fileWatcher); - } - - const configurationWatcher = vscode.workspace.onDidChangeConfiguration( - preFetchConfiguration - ); - disposables.push(configurationWatcher); - - return disposables; -} - -export function getConfiguration(): Readonly { - const currentFile = vscode.window.activeTextEditor?.document.uri; - - if (!currentFile) { - return getWorkspaceConfiguration(); - } - - const workSpaceKey = vscode.workspace.getWorkspaceFolder(currentFile)?.name; - - if (!workSpaceKey) { - return getWorkspaceConfiguration(); - } - - return cachedConfiguration.get(workSpaceKey) || getWorkspaceConfiguration(); -} - -async function preFetchConfiguration() { - for (const workfolder of vscode.workspace.workspaceFolders || []) { - const configuration = await getConfigurationForWorkfolder(workfolder); - cachedConfiguration.set(workfolder.name, configuration); - } -} - -async function getConfigurationForWorkfolder( - workfolder: vscode.WorkspaceFolder -): Promise { - const workspaceConfiguration = getWorkspaceConfiguration(); - const workfolderTSConfig = await getWorkfolderTsConfigConfiguration( - workfolder - ); - return { - ...workspaceConfiguration, - ...workfolderTSConfig - }; -} - -async function getWorkfolderTsConfigConfiguration( - workfolder: vscode.WorkspaceFolder -): Promise> { - const include = new vscode.RelativePattern(workfolder, "[tj]sconfig.json"); - const exclude = new vscode.RelativePattern(workfolder, "**/node_modules/**"); - - const files = await vscode.workspace.findFiles(include, exclude); - - const mappingsFromWorkspaceConfig = files.reduce( - (mappings: Mapping[], file) => { - try { - const parsedFile = JSON5.parse(readFileSync(file.fsPath).toString()); - const newMappings = createMappingsFromWorkspaceConfig(parsedFile); - return [...mappings, ...newMappings]; - } catch (error) { - return mappings; - } - }, - [] - ); - - const configuration = vscode.workspace.getConfiguration("path-intellisense"); - const rawMappings = configuration["mappings"]; - const parsedMappings = parseMappings(rawMappings); - - const allMappings = [...parsedMappings, ...mappingsFromWorkspaceConfig]; - const mappingsWithCorrectPath = replaceWorkspaceRoot(allMappings, workfolder); - - return { - mappings: mappingsWithCorrectPath - }; -} - -function getWorkspaceConfiguration(): Config { - const cfgExtension = vscode.workspace.getConfiguration("path-intellisense"); - const cfgGeneral = vscode.workspace.getConfiguration("files"); + const cfgExtension = getConfig("path-intellisense"); + const cfgGeneral = getConfig("files"); + const mappings = await getMappings(cfgExtension, workspaceFolder); return { autoSlash: cfgExtension["autoSlashAfterDirectory"], @@ -105,51 +21,16 @@ function getWorkspaceConfiguration(): Config { withExtension: cfgExtension["extensionOnImport"], absolutePathToWorkspace: cfgExtension["absolutePathToWorkspace"], filesExclude: cfgGeneral["exclude"], - mappings: [] + mappings, }; } -/** - * From { "lib": "libraries", "other": "otherpath" } - * To [ { key: "lib", value: "libraries" }, { key: "other", value: "otherpath" } ] - * @param mappings { "lib": "libraries" } - */ -function parseMappings(mappings: { [key: string]: string }): Mapping[] { - return Object.entries(mappings).map(([key, value]) => ({ key, value })); -} - -/** - * Replace ${workspaceRoot} with workfolder.uri.path - * @param mappings - * @param workfolder - */ -function replaceWorkspaceRoot( - mappings: Mapping[], - workfolder: vscode.WorkspaceFolder -): Mapping[] { - return mappings.map(mapping => { - return { - key: mapping.key, - value: mapping.value.replace("${workspaceRoot}", workfolder.uri.path) - }; - }); -} - -function createMappingsFromWorkspaceConfig(tsconfig: { - compilerOptions: { baseUrl: string }; -}): Mapping[] { - const mappings: Mapping[] = []; - const baseUrl = tsconfig?.compilerOptions?.baseUrl; - - if (baseUrl) { - mappings.push({ - key: baseUrl, - // value: `${workfolder.uri.path}/${baseUrl}` - value: "${workspaceRoot}/" + baseUrl - }); - } - - // Todo: paths property - - return mappings; +async function getMappings( + configuration: vscode.WorkspaceConfiguration, + workfolder?: vscode.WorkspaceFolder +): Promise { + const mappings = parseMappings(configuration["mappings"]); + const tsConfigMappings = await getWorkfolderTsConfigConfiguration(workfolder); + const allMappings = [...mappings, ...tsConfigMappings]; + return replaceWorkspaceRoot(allMappings, workfolder); } diff --git a/src/configuration/mapping.service.ts b/src/configuration/mapping.service.ts new file mode 100644 index 0000000..56a9b66 --- /dev/null +++ b/src/configuration/mapping.service.ts @@ -0,0 +1,34 @@ +import { Mapping } from "./configuration.interface"; +import * as vscode from "vscode"; + +/** + * From { "lib": "libraries", "other": "otherpath" } + * To [ { key: "lib", value: "libraries" }, { key: "other", value: "otherpath" } ] + * @param mappings { "lib": "libraries" } + */ +export function parseMappings(mappings: { [key: string]: string }): Mapping[] { + return Object.entries(mappings).map(([key, value]) => ({ key, value })); +} + +/** + * Replace ${workspaceRoot} with workfolder.uri.path + * @param mappings + * @param workfolder + */ +export function replaceWorkspaceRoot( + mappings: Mapping[], + workfolder?: vscode.WorkspaceFolder +): Mapping[] { + const rootPath = workfolder?.uri.path; + + if (rootPath) { + return mappings.map(({ key, value }) => ({ + key, + value: value.replace("${workspaceRoot}", rootPath), + })); + } else { + return mappings.filter( + ({ value }) => value.indexOf("${workspaceRoot}") === -1 + ); + } +} diff --git a/src/configuration/tsconfig.service.ts b/src/configuration/tsconfig.service.ts new file mode 100644 index 0000000..33790d4 --- /dev/null +++ b/src/configuration/tsconfig.service.ts @@ -0,0 +1,85 @@ +import * as vscode from "vscode"; +import * as JSON5 from "json5"; +import { readFileSync } from "fs"; +import { Mapping } from "./configuration.interface"; + +export const getWorkfolderTsConfigConfiguration = memoize(async function ( + workfolder: vscode.WorkspaceFolder +): Promise { + const include = new vscode.RelativePattern(workfolder, "[tj]sconfig.json"); + const exclude = new vscode.RelativePattern(workfolder, "**/node_modules/**"); + const files = await vscode.workspace.findFiles(include, exclude); + + return files.reduce((mappings: Mapping[], file) => { + try { + const parsedFile = JSON5.parse(readFileSync(file.fsPath).toString()); + const newMappings = createMappingsFromWorkspaceConfig(parsedFile); + return [...mappings, ...newMappings]; + } catch (error) { + return mappings; + } + }, []); +}); + +export function subscribeToTsConfigChanges(): vscode.Disposable[] { + const disposables: vscode.Disposable[] = []; + for (const workfolder of vscode.workspace.workspaceFolders || []) { + const pattern = new vscode.RelativePattern(workfolder, "[tj]sconfig.json"); + const fileWatcher = vscode.workspace.createFileSystemWatcher(pattern); + fileWatcher.onDidChange(() => invalidateCache(workfolder)); + disposables.push(fileWatcher); + } + return disposables; +} + +function createMappingsFromWorkspaceConfig(tsconfig: { + compilerOptions: { baseUrl: string }; +}): Mapping[] { + const mappings: Mapping[] = []; + const baseUrl = tsconfig?.compilerOptions?.baseUrl; + + if (baseUrl) { + mappings.push({ + key: baseUrl, + // value: `${workfolder.uri.path}/${baseUrl}` + value: "${workspaceRoot}/" + baseUrl, + }); + } + + // Todo: paths property + + return mappings; +} + +/** Caching */ + +let cachedMappings = new Map(); + +function memoize( + fn: (workfolder: vscode.WorkspaceFolder) => Promise +) { + async function cachedFunction( + workfolder?: vscode.WorkspaceFolder + ): Promise { + if (!workfolder) { + return Promise.resolve([]); + } + + const key = workfolder.name; + const cachedMapping = cachedMappings.get(key); + + if (cachedMapping) { + return cachedMapping; + } else { + let result = await fn(workfolder); + cachedMappings.set(key, result); + return result; + } + } + + return cachedFunction; +} + +function invalidateCache(workfolder: vscode.WorkspaceFolder) { + cachedMappings.delete(workfolder.name); +} diff --git a/src/extension.ts b/src/extension.ts index 98b0af5..fe8465e 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -2,23 +2,21 @@ // Import the module and reference it with the alias vscode in your code below import { ExtensionContext, languages } from "vscode"; import { providers } from "./providers"; -import { subscribeToConfigurationService } from "./configuration/configuration.service"; +import { subscribeToTsConfigChanges } from "./configuration/tsconfig.service"; // this method is called when your extension is activated // your extension is activated the very first time the command is executed export function activate(context: ExtensionContext) { /** - * Subscribe to the configuration service + * Subscribe to the ts config changes */ - subscribeToConfigurationService().then(configurationServiceSubscription => { - context.subscriptions.push(...configurationServiceSubscription); - }); + context.subscriptions.push(...subscribeToTsConfigChanges()); /** * Register Providers * Add new providers in src/providers/ * */ - providers.forEach(provider => { + providers.forEach((provider) => { const disposable = languages.registerCompletionItemProvider( provider.selector, provider.provider, diff --git a/src/providers/javascript/javascript.provider.ts b/src/providers/javascript/javascript.provider.ts index bdd8f54..b0d5c72 100644 --- a/src/providers/javascript/javascript.provider.ts +++ b/src/providers/javascript/javascript.provider.ts @@ -21,12 +21,12 @@ export const JavaScriptProvider: PathIntellisenseProvider = { triggerCharacters: ["/", '"', "'"], }; -function provideCompletionItems( +async function provideCompletionItems( document: vscode.TextDocument, position: vscode.Position -): Thenable { +): Promise { const context = createContext(document, position); - const config = getConfiguration(); + const config = await getConfiguration(document.uri); return shouldProvide(context, config) ? provide(context, config) @@ -74,7 +74,7 @@ async function provide( context.document.uri.fsPath, context.fromString, rootPath, - [] + config.mappings ); const childrenOfPath = await getChildrenOfPath(path, config); diff --git a/src/test/suite/configuration.service.test.ts b/src/test/suite/configuration.service.test.ts index fb9d2ca..d19d2ac 100644 --- a/src/test/suite/configuration.service.test.ts +++ b/src/test/suite/configuration.service.test.ts @@ -4,22 +4,20 @@ import { resolve } from "path"; // You can import and use all API from the 'vscode' module // as well as import your extension to test it import * as vscode from "vscode"; -import { - subscribeToConfigurationService, - getConfiguration -} from "../../configuration/configuration.service"; +import { getConfiguration } from "../../configuration/configuration.service"; +import { subscribeToTsConfigChanges } from "../../configuration/tsconfig.service"; suite("Configuration Service", () => { vscode.window.showInformationMessage("Start all tests."); test("has different configuration for the workspaceFolders", async () => { - await subscribeToConfigurationService(); + await subscribeToTsConfigChanges(); - await openDocument("demo-workspace/project-one/index.js"); - const configurationProjectOne = getConfiguration(); + const document = await openDocument("demo-workspace/project-one/index.js"); + const configurationProjectOne = await getConfiguration(document.uri); - await openDocument("demo-workspace/project-two/index.js"); - const configurationProjectTwo = getConfiguration(); + const document2 = await openDocument("demo-workspace/project-two/index.js"); + const configurationProjectTwo = await getConfiguration(document2.uri); assert.equal(configurationProjectOne?.absolutePathToWorkspace, true); assert.equal(configurationProjectOne?.withExtension, true); @@ -54,14 +52,16 @@ suite("Configuration Service", () => { test("still can load the config with a wrong ts config", async () => { assert.doesNotThrow(async () => { - await subscribeToConfigurationService(); + await subscribeToTsConfigChanges(); }); }); test("has default configuration for non project folder files", async () => { - await subscribeToConfigurationService(); - await openDocument("demo-workspace/file-outside-folders.js"); - const configuration = getConfiguration(); + await subscribeToTsConfigChanges(); + const document = await openDocument( + "demo-workspace/file-outside-folders.js" + ); + const configuration = await getConfiguration(document.uri); assert.equal(configuration?.absolutePathToWorkspace, true); assert.equal(configuration?.withExtension, true); @@ -69,11 +69,13 @@ suite("Configuration Service", () => { }); test("updates configuration on tsconfig change", async () => { - await subscribeToConfigurationService(); + await subscribeToTsConfigChanges(); // Read existing configuration - await openDocument("demo-workspace/project-one/index.js"); - const configuration = getConfiguration(); + const documentOne = await openDocument( + "demo-workspace/project-one/index.js" + ); + const configuration = await getConfiguration(documentOne.uri); // change tsconfig file const absoluteUrl = getAbsoluteUrl( @@ -81,21 +83,23 @@ suite("Configuration Service", () => { ); const document = await vscode.workspace.openTextDocument(absoluteUrl); await vscode.window.showTextDocument(document); - await vscode.window.activeTextEditor?.edit(editbuilder => { + await vscode.window.activeTextEditor?.edit((editbuilder) => { editbuilder.replace(new vscode.Range(2, 24, 2, 27), "bla"); }); await document.save(); // wait for the configuration to be updated.. - await new Promise(resolve => + await new Promise((resolve) => setTimeout(() => { resolve(); }, 1500) ); // Read existing configuration - await openDocument("demo-workspace/project-one/index.js"); - const newConfiguration = getConfiguration(); + const otherDocument = await openDocument( + "demo-workspace/project-one/index.js" + ); + const newConfiguration = await getConfiguration(otherDocument.uri); assert.equal( configuration?.mappings[1].value.endsWith( @@ -113,7 +117,7 @@ suite("Configuration Service", () => { // Clean up await vscode.window.showTextDocument(document); - await vscode.window.activeTextEditor?.edit(editbuilder => { + await vscode.window.activeTextEditor?.edit((editbuilder) => { editbuilder.replace(new vscode.Range(2, 24, 2, 27), "one"); }); await document.save(); @@ -124,6 +128,7 @@ async function openDocument(relativeUri: string) { const absoluteUrl = getAbsoluteUrl(relativeUri); const document = await vscode.workspace.openTextDocument(absoluteUrl); await vscode.window.showTextDocument(document); + return document; } function getAbsoluteUrl(relativeUri: string) {