Skip to content

Commit

Permalink
fix: glob support for non-file urls (#3447)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jason3S authored Jul 17, 2024
1 parent 435032e commit 714ed0d
Show file tree
Hide file tree
Showing 5 changed files with 280 additions and 89 deletions.
37 changes: 24 additions & 13 deletions packages/_server/src/config/WorkspacePathResolver.mts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { logError } from '@internal/common-utils/log';
import { uriToFilePathOrHref } from '@internal/common-utils/uriHelper';
import type { BaseSetting, Glob, GlobDef } from 'cspell-lib';
import * as os from 'os';
import * as Path from 'path';
import type { WorkspaceFolder } from 'vscode-languageserver/node.js';
import { URI as Uri } from 'vscode-uri';

import type { CSpellUserSettings } from './cspellConfig/index.mjs';
import { extractDictionaryDefinitions, extractDictionaryList } from './customDictionaries.mjs';
import { toDirURL } from './urlUtil.mjs';

export type WorkspaceGlobResolverFn = (glob: Glob) => GlobDef;
export type WorkspacePathResolverFn = (path: string) => string;
Expand Down Expand Up @@ -103,36 +103,41 @@ function createWorkspaceNameToGlobResolver(
): (globRoot: string | undefined) => WorkspaceGlobResolverFn {
const _folder = { ...folder };
const _folders = [...folders];
return (globRoot: string | undefined) => {
const folderPairs = [['${workspaceFolder}', _folder.path] as [string, string]].concat(
_folders.map((folder) => [`\${workspaceFolder:${folder.name}}`, folder.path]),
return (globRoot: string | URL | undefined) => {
const folderPairs = [['${workspaceFolder}', toDirURL(_folder.path)] as [string, URL]].concat(
_folders.map((folder) => [`\${workspaceFolder:${folder.name}}`, toDirURL(folder.path)]),
);
workspaceRoot = workspaceRoot || _folder.path;
const map = new Map(folderPairs);
const regEx = /^\$\{workspaceFolder(?:[^}]*)\}/i;
const root = resolveRoot(globRoot || '${workspaceFolder}');

function lookUpWorkspaceFolder(match: string): string {
function lookUpWorkspaceFolder(match: string): URL {
const r = map.get(match);
if (r !== undefined) return r;
logError(`Failed to resolve ${match}`);
return match;
return toDirURL(match);
}

function resolveRoot(globRoot: string | undefined): string | undefined {
function resolveRoot(globRoot: string | URL | undefined): URL | undefined {
if (globRoot instanceof URL) return globRoot;
globRoot = globRoot?.startsWith('~') ? os.homedir() + globRoot.slice(1) : globRoot;
const matchRoot = globRoot?.match(regEx);
if (matchRoot && globRoot) {
const workspaceRoot = lookUpWorkspaceFolder(matchRoot[0]);
return Path.join(workspaceRoot, globRoot.slice(matchRoot[0].length));
let path = globRoot.slice(matchRoot[0].length).replaceAll('\\', '/');
path = path.startsWith('/') ? path.slice(1) : path;
// console.log('matchRoot: %o', { globRoot, matchRoot, path, workspaceRoot: workspaceRoot.href });
return new URL(path, workspaceRoot);
}
return globRoot;
return globRoot ? toDirURL(globRoot) : undefined;
}

return (glob: Glob) => {
function resolver(glob: Glob) {
if (typeof glob == 'string') {
glob = {
glob,
root,
root: root?.href,
};
}

Expand All @@ -142,14 +147,20 @@ function createWorkspaceNameToGlobResolver(
return {
...glob,
glob: glob.glob.slice(matchGlob[0].length),
root,
root: root.href,
};
}

return {
...glob,
root: resolveRoot(glob.root),
root: resolveRoot(glob.root)?.href,
};
}

return (glob: Glob) => {
const r = resolver(glob);
// console.log('resolveGlob: %o -> %o', glob, r);
return r;
};
};
}
Expand Down
54 changes: 31 additions & 23 deletions packages/_server/src/config/WorkspacePathResolver.test.mts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { homedir } from 'node:os';
import { fileURLToPath, pathToFileURL } from 'node:url';

import { logError } from '@internal/common-utils/log';
import * as Path from 'path';
import { describe, expect, type Mock, test, vi } from 'vitest';
import type { WorkspaceFolder } from 'vscode-languageserver/node.js';
import { URI as Uri } from 'vscode-uri';

import type { CSpellUserSettings, CustomDictionaries } from './cspellConfig/index.mjs';
import { normalizeWindowsRoot, normalizeWindowsUrl, toDirURL, uriToGlobRoot } from './urlUtil.mjs';
import { createWorkspaceNamesResolver, debugExports, resolveSettings } from './WorkspacePathResolver.mjs';

vi.mock('vscode-languageserver/node');
Expand Down Expand Up @@ -81,10 +85,14 @@ describe('Validate workspace substitution resolver', () => {
const clientPath = Path.join(rootPath, 'client');
const serverPath = Path.join(rootPath, '_server');
const clientTestPath = Path.join(clientPath, 'test');
const rootFolderUri = Uri.file(rootPath);
const clientUri = Uri.file(clientPath);
const serverUri = Uri.file(serverPath);
const testUri = Uri.file(clientTestPath);
const rootFolderUrl = pathToFileURL(rootPath);
const clientUrl = pathToFileURL(clientPath);
const serverUrl = pathToFileURL(serverPath);
const testUrl = pathToFileURL(clientTestPath);
const rootFolderUri = Uri.parse(rootFolderUrl.href);
const clientUri = Uri.parse(clientUrl.href);
const serverUri = Uri.parse(serverUrl.href);
const testUri = Uri.parse(testUrl.href);
const workspaceFolders = {
root: {
name: 'Root Folder',
Expand All @@ -105,10 +113,10 @@ describe('Validate workspace substitution resolver', () => {
};

const paths = {
root: uriToFsPath(workspaceFolders.root.uri),
client: uriToFsPath(workspaceFolders.client.uri),
server: uriToFsPath(workspaceFolders.server.uri),
test: uriToFsPath(workspaceFolders.test.uri),
root: uriToRoot(workspaceFolders.root.uri),
client: uriToRoot(workspaceFolders.client.uri),
server: uriToRoot(workspaceFolders.server.uri),
test: uriToRoot(workspaceFolders.test.uri),
};

const workspaces: WorkspaceFolder[] = [workspaceFolders.root, workspaceFolders.client, workspaceFolders.server, workspaceFolders.test];
Expand Down Expand Up @@ -251,19 +259,19 @@ describe('Validate workspace substitution resolver', () => {
// '${workspaceFolder:Server}/samples/**',
// '${workspaceFolder:client-test}/**/*.json',
expect(result.ignorePaths).toEqual([
{ glob: '**/node_modules/**', root: uriToFsPath(workspaceFolders.client.uri) },
{ glob: '/node_modules/**', root: uriToFsPath(workspaceFolders.client.uri) },
{ glob: '/samples/**', root: uriToFsPath(workspaceFolders.server.uri) },
{ glob: '/**/*.json', root: uriToFsPath(workspaceFolders.test.uri) },
{ glob: 'dist/**', root: uriToFsPath(workspaceFolders.server.uri) },
{ glob: '**/node_modules/**', root: uriToRoot(workspaceFolders.client.uri) },
{ glob: '/node_modules/**', root: uriToRoot(workspaceFolders.client.uri) },
{ glob: '/samples/**', root: uriToRoot(workspaceFolders.server.uri) },
{ glob: '/**/*.json', root: uriToRoot(workspaceFolders.test.uri) },
{ glob: 'dist/**', root: uriToRoot(workspaceFolders.server.uri) },
]);
});

test.each`
files | globRoot | expected
${undefined} | ${undefined} | ${undefined}
${['**']} | ${undefined} | ${[{ glob: '**', root: paths.client }]}
${['**']} | ${'~/glob-root'} | ${[{ glob: '**', root: '~/glob-root' }]}
${['**']} | ${'~/glob-root'} | ${[{ glob: '**', root: normalizeWindowsUrl(new URL('glob-root/', pathToFileURL(homedir() + '/'))).href }]}
${['**']} | ${'${workspaceFolder}/..'} | ${[{ glob: '**', root: paths.root }]}
${['${workspaceFolder}/**']} | ${''} | ${[{ glob: '/**', root: paths.client }]}
${['${workspaceFolder}/**']} | ${undefined} | ${[{ glob: '/**', root: paths.client }]}
Expand Down Expand Up @@ -320,29 +328,29 @@ describe('Validate workspace substitution resolver', () => {
[
{
glob: '*.md',
root: uriToFsPath(workspaceFolders.client.uri),
root: uriToGlobRoot(workspaceFolders.client.uri),
},
{
glob: '**/*.ts',
root: uriToFsPath(workspaceFolders.client.uri),
root: uriToGlobRoot(workspaceFolders.client.uri),
},
{
glob: '**/*.js',
root: uriToFsPath(workspaceFolders.client.uri),
root: uriToGlobRoot(workspaceFolders.client.uri),
},
],
{
glob: '/docs/nl_NL/**',
root: uriToFsPath(workspaceFolders.client.uri),
root: uriToGlobRoot(workspaceFolders.client.uri),
},
[
{
glob: '/**/*.config.json',
root: uriToFsPath(workspaceFolders.client.uri),
root: uriToGlobRoot(workspaceFolders.client.uri),
},
{
glob: '**/*.config.json',
root: uriToFsPath(workspaceFolders.server.uri),
root: uriToGlobRoot(workspaceFolders.server.uri),
},
],
]);
Expand Down Expand Up @@ -462,11 +470,11 @@ describe('Validate workspace substitution resolver', () => {
expect(mockLogError).toHaveBeenCalledWith('Failed to resolve ${workspaceFolder:_server}');
});

function uriToFsPath(u: string | Uri): string {
function uriToRoot(u: string | Uri): string {
if (typeof u === 'string') {
u = Uri.parse(u);
}
return u.fsPath;
return toDirURL(u).href;
}

function normalizePath<T extends { path?: string }>(values?: T[]): T[] | undefined {
Expand All @@ -483,6 +491,6 @@ describe('Validate workspace substitution resolver', () => {
}

function p(path: string): string {
return Uri.file(path).fsPath;
return normalizeWindowsRoot(fileURLToPath(pathToFileURL(path)));
}
});
76 changes: 23 additions & 53 deletions packages/_server/src/config/documentSettings.mts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import { toFileUri, toUri } from '@internal/common-utils/uriHelper';
import { GitIgnore } from 'cspell-gitignore';
import type { GlobMatchOptions, GlobMatchRule, GlobPatternNormalized } from 'cspell-glob';
import { GlobMatcher } from 'cspell-glob';
import { isUrlLike } from 'cspell-io';
import type { ExcludeFilesGlobMap } from 'cspell-lib';
import {
calcOverrideSettings,
Expand All @@ -40,14 +39,15 @@ import type { DocumentUri, ServerSideApi, VSCodeSettingsCspell, WorkspaceConfigF
import { extensionId } from '../constants.mjs';
import { uniqueFilter } from '../utils/index.mjs';
import { findMatchingFoldersForUri } from '../utils/matchingFoldersForUri.mjs';
import { findRepoRoot, stat } from '../vfs/index.mjs';
import { findRepoRoot } from '../vfs/index.mjs';
import type { VSConfigAdvanced } from './cspellConfig/cspellConfig.mjs';
import { filterMergeFields } from './cspellConfig/cspellMergeFields.mjs';
import type { EnabledSchemes } from './cspellConfig/FileTypesAndSchemeSettings.mjs';
import type { CSpellUserSettings } from './cspellConfig/index.mjs';
import { canAddWordsToDictionary } from './customDictionaries.mjs';
import { handleSpecialUri } from './docUriHelper.mjs';
import { applyEnabledFileTypes, applyEnabledSchemes, extractEnabledFileTypes, extractEnabledSchemes } from './extractEnabledFileTypes.mjs';
import { filterUrl, toDirURL, tryJoinURL, uriToGlobPath, uriToGlobRoot, urlToFilepath } from './urlUtil.mjs';
import type { TextDocumentUri } from './vscode.config.mjs';
import { getConfiguration, getWorkspaceFolders } from './vscode.config.mjs';
import { createWorkspaceNamesResolver, resolveSettings } from './WorkspacePathResolver.mjs';
Expand Down Expand Up @@ -213,7 +213,7 @@ export class DocumentSettings {
private async _isGitIgnored(extSettings: ExtSettings, uri: Uri): Promise<boolean | undefined> {
if (!canCheckAgainstGlob(uri)) return undefined;
if (!extSettings.settings.useGitignore) return undefined;
return await this.gitIgnore.isIgnored(uriToGlobPath(uri));
return await this.gitIgnore.isIgnored(urlToFilepath(uri));
}

/**
Expand Down Expand Up @@ -453,6 +453,7 @@ export class DocumentSettings {
);
const settings = vscodeCSpellConfigSettingsForDocument.noConfigSearch ? undefined : await searchForConfig(useURLForConfig);
const rootFolder = this.rootSchemaAndDomainFolderForUri(docUri);
// console.log('__fetchSettingsForUri Root Folder: %o', { rootFolder, docUri });
const folder = await this.findMatchingFolder(docUri, folders[0] || rootFolder);
const globRootFolder = folder !== rootFolder ? folder : folders[0] || folder;

Expand All @@ -473,15 +474,16 @@ export class DocumentSettings {
fileSettings = applyEnabledSchemes(fileSettings, enabledSchemes);
const { ignorePaths = [], files = [] } = fileSettings;

const globRoot = uriToGlobPath(globRootFolder.uri);
const globRoot = uriToGlobRoot(globRootFolder.uri);
if (!files.length && vscodeCSpellConfigSettingsForDocument.spellCheckOnlyWorkspaceFiles !== false) {
// Add file globs that will match the entire workspace.
folders.forEach((folder) => files.push({ glob: '/**', root: uriToGlobPath(folder.uri) }));
folders.forEach((folder) => files.push({ glob: '/**', root: uriToGlobRoot(folder.uri) }));
fileSettings.enableGlobDot = fileSettings.enableGlobDot ?? true;
}
fileSettings.files = files;

const globs = ignorePaths.concat(defaultExclude);
// console.log('Glob Root: %o', { globRoot: globRoot, docUri });
const excludeGlobMatcher = new GlobMatcher(globs, globRoot);
const includeOptions: GlobMatchOptions = { root: globRoot, mode: 'include' };
setIfDefined(includeOptions, 'dot', fileSettings.enableGlobDot);
Expand Down Expand Up @@ -805,56 +807,24 @@ export function isExcluded(settings: ExtSettings, uri: Uri): boolean {
return settings.excludeGlobMatcher.match(uriToGlobPath(uri));
}

async function filterUrl(uri: Uri): Promise<Uri | undefined> {
const url = new URL(uri.toString());
try {
const stats = await stat(url);
const found = stats.isFile() ? uri : undefined;
return found;
} catch {
return undefined;
}
}

/**
* See if it is possible to join the rel to the base.
* This helps detect `untitled:untitled-1` uri's that are not valid.
* @param rel - relative path
* @param base - base URL
* @returns the joined path or undefined if it is not possible.
*/
function tryJoinURL(rel: string, base: URL | string): URL | undefined {
try {
return new URL(rel, base);
} catch {
return undefined;
}
}

function toDirURL(url: string | URL): URL {
if (url instanceof URL) {
if (url.pathname.endsWith('/')) {
return url;
}
url = url.href;
}
url = new URL(url);
if (!url.pathname.endsWith('/')) {
url.pathname += '/';
}
return url;
}

function uriToGlobPath(uri: string | URL | Uri): string {
if (typeof uri === 'string' && !isUrlLike(uri)) {
return uri;
}
const u = toFileUri(uri);
return u.scheme === 'file' ? u.fsPath : u.path;
}
const checkScheme: Record<string, boolean | undefined> = {
comment: false,
file: true,
gist: false,
repo: true,
sftp: true,
untitled: true,
'vscode-notebook-cell': true,
'vscode-scm': false,
'vscode-userdata': false,
'vscode-vfs': true,
vsls: true,
};

function canCheckAgainstGlob(uri: Uri): boolean {
return uri.scheme === 'file';
const r = checkScheme[uri.scheme] ?? false;
// console.log('canCheckAgainstGlob %o %o', uri.toString(true), r);
return r;
}

export const __testing__ = {
Expand Down
Loading

0 comments on commit 714ed0d

Please sign in to comment.