Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 5 additions & 213 deletions client/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,11 @@ import type {
Location,
Position,
} from "vscode-languageclient/node";
import { getWorkspacesEnabledInfo, isPathEnabled } from "./enable";
import { getWorkspacesEnabledInfo } from "./enable";
import { denoUpgradePromptAndExecute } from "./upgrade";
import * as fs from "fs";
import * as path from "path";
import * as process from "process";
import * as jsoncParser from "jsonc-parser/lib/esm/main.js";
import { semver } from "./semver";

// deno-lint-ignore no-explicit-any
Expand Down Expand Up @@ -72,11 +71,12 @@ export function cacheActiveDocument(
}

export function clearHiddenPromptStorage(
context: vscode.ExtensionContext,
_context: vscode.ExtensionContext,
_extensionContext: DenoExtensionContext,
): Callback {
return () => {
context.globalState.update("deno.tsConfigPathsWithPromptHidden", []);
// Reset states for 'Don't show this again' prompt responses, etc.
// Currently unused.
};
}

Expand Down Expand Up @@ -252,9 +252,8 @@ export function startLanguageServer(
extensionContext.clientSubscriptions.push(
extensionContext.client.onNotification(
"deno/didChangeDenoConfiguration",
async ({ changes }: DidChangeDenoConfigurationParams) => {
({ changes }: DidChangeDenoConfigurationParams) => {
let changedScopes = false;
const addedDenoJsonUris = [];
for (const change of changes) {
if (change.configurationType != "denoJson") {
continue;
Expand All @@ -263,7 +262,6 @@ export function startLanguageServer(
const scopePath = vscode.Uri.parse(change.scopeUri).fsPath;
scopesWithDenoJson.add(scopePath);
changedScopes = true;
addedDenoJsonUris.push(vscode.Uri.parse(change.fileUri));
} else if (change.type == "removed") {
const scopePath = vscode.Uri.parse(change.scopeUri).fsPath;
scopesWithDenoJson.delete(scopePath);
Expand All @@ -274,13 +272,6 @@ export function startLanguageServer(
extensionContext.tsApi?.refresh();
}
extensionContext.tasksSidebar.refresh();
for (const addedDenoJsonUri of addedDenoJsonUris) {
await maybeShowTsConfigPrompt(
context,
extensionContext,
addedDenoJsonUri,
);
}
},
),
);
Expand Down Expand Up @@ -358,167 +349,6 @@ function showWelcomePageIfFirstUse(
}
}

function isObject(value: unknown) {
return value && typeof value == "object" && !Array.isArray(value);
}

/**
* For a discovered deno.json file, see if there's an adjacent tsconfig.json.
* Offer options to either copy over the compiler options from it, or disable
* the Deno LSP if it contains plugins.
*/
async function maybeShowTsConfigPrompt(
context: vscode.ExtensionContext,
extensionContext: DenoExtensionContext,
denoJsonUri: vscode.Uri,
) {
const denoJsonPath = denoJsonUri.fsPath;
if (!isPathEnabled(extensionContext, denoJsonPath)) {
return;
}
const scopePath = path.dirname(denoJsonPath) + path.sep;
const tsConfigPath = path.join(scopePath, "tsconfig.json");
const tsConfigPathsWithPromptHidden = context.globalState.get<string[]>(
"deno.tsConfigPathsWithPromptHidden",
) ?? [];
if (tsConfigPathsWithPromptHidden?.includes?.(tsConfigPath)) {
return;
}
let tsConfigContent;
try {
const tsConfigText = await fs.promises.readFile(tsConfigPath, {
encoding: "utf8",
});
tsConfigContent = jsoncParser.parse(tsConfigText);
} catch {
return;
}
const compilerOptions = tsConfigContent?.compilerOptions;
if (!isObject(compilerOptions)) {
return;
}
for (const key in compilerOptions) {
if (!ALLOWED_COMPILER_OPTIONS.includes(key)) {
delete compilerOptions[key];
}
}
if (Object.entries(compilerOptions).length == 0) {
return;
}
const plugins = compilerOptions?.plugins;
let selection;
if (Array.isArray(plugins) && plugins.length) {
// This tsconfig.json contains plugins. Prompt to disable the LSP.
const workspaceFolders = vscode.workspace.workspaceFolders ?? [];
let scopeFolderEntry = null;
const folderEntries = workspaceFolders.map((f) =>
[f.uri.fsPath + path.sep, f] as const
);
folderEntries.sort();
folderEntries.reverse();
for (const folderEntry of folderEntries) {
if (scopePath.startsWith(folderEntry[0])) {
scopeFolderEntry = folderEntry;
break;
}
}
if (!scopeFolderEntry) {
return;
}
const [scopeFolderPath, scopeFolder] = scopeFolderEntry;
selection = await vscode.window.showInformationMessage(
`A tsconfig.json with compiler options was discovered in a Deno-enabled folder. For projects with compiler plugins, it is recommended to disable the Deno language server if you are seeing errors (${tsConfigPath}).`,
"Disable Deno LSP",
"Hide this message",
);
if (selection == "Disable Deno LSP") {
const config = vscode.workspace.getConfiguration(
EXTENSION_NS,
scopeFolder,
);
if (scopePath == scopeFolderPath) {
await config.update("enable", false);
} else {
let disablePaths = config.get<string[]>("disablePaths");
if (!Array.isArray(disablePaths)) {
disablePaths = [];
}
const relativeUri = scopePath.substring(scopeFolderPath.length).replace(
/\\/g,
"/",
).replace(/\/*$/, "");
disablePaths.push(relativeUri);
await config.update("disablePaths", disablePaths);
}
}
} else {
// This tsconfig.json has compiler options which may be copied to a
// deno.json. If the deno.json has no compiler options, prompt to copy them
// over.
let denoJsonText;
let denoJsonContent;
try {
denoJsonText = await fs.promises.readFile(denoJsonPath, {
encoding: "utf8",
});
denoJsonContent = jsoncParser.parse(denoJsonText);
} catch {
return;
}
if (!isObject(denoJsonContent) || "compilerOptions" in denoJsonContent) {
return;
}
selection = await vscode.window.showInformationMessage(
`A tsconfig.json with compiler options was discovered in a Deno-enabled folder. Would you like to copy these to your Deno configuration file? Note that only a subset of options are supported (${tsConfigPath}).`,
"Copy to deno.json[c]",
"Hide this message",
);
if (selection == "Copy to deno.json[c]") {
try {
let newDenoJsonContent = jsoncParser.applyEdits(
denoJsonText,
jsoncParser.modify(
denoJsonText,
["compilerOptions"],
compilerOptions,
{ formattingOptions: { insertSpaces: true, tabSize: 2 } },
),
);
const unstable = Array.isArray(denoJsonContent.unstable)
? denoJsonContent.unstable as unknown[]
: [];
if (!unstable.includes("sloppy-imports")) {
unstable.push("sloppy-imports");
newDenoJsonContent = jsoncParser.applyEdits(
newDenoJsonContent,
jsoncParser.modify(
newDenoJsonContent,
["unstable"],
unstable,
{ formattingOptions: { insertSpaces: true, tabSize: 2 } },
),
);
}
await fs.promises.writeFile(denoJsonPath, newDenoJsonContent);
} catch (error) {
vscode.window.showErrorMessage(
`Could not modify "${denoJsonPath}": ${error}`,
);
}
}
}
if (selection == "Hide this message") {
const tsConfigPathsWithPromptHidden = context.globalState.get<string[]>(
"deno.tsConfigPathsWithPromptHidden",
) ?? [];
tsConfigPathsWithPromptHidden?.push?.(tsConfigPath);
context.globalState.update(
"deno.tsConfigPathsWithPromptHidden",
tsConfigPathsWithPromptHidden,
);
}
}

export function showReferences(
_content: vscode.ExtensionContext,
extensionContext: DenoExtensionContext,
Expand Down Expand Up @@ -726,41 +556,3 @@ function isDenoDisabledCompletely(): boolean {
vscode.workspace.getConfiguration(EXTENSION_NS, f)
).every(isScopeDisabled);
}

// Keep this in sync with the supported compiler options set in CLI. Currently:
// https://github.com/denoland/deno_config/blob/0.47.1/src/deno_json/ts.rs#L85-L119
const ALLOWED_COMPILER_OPTIONS = [
"allowUnreachableCode",
"allowUnusedLabels",
"checkJs",
"emitDecoratorMetadata",
"exactOptionalPropertyTypes",
"experimentalDecorators",
"isolatedDeclarations",
"jsx",
"jsxFactory",
"jsxFragmentFactory",
"jsxImportSource",
"jsxPrecompileSkipElements",
"lib",
"noErrorTruncation",
"noFallthroughCasesInSwitch",
"noImplicitAny",
"noImplicitOverride",
"noImplicitReturns",
"noImplicitThis",
"noPropertyAccessFromIndexSignature",
"noUncheckedIndexedAccess",
"noUnusedLocals",
"noUnusedParameters",
"rootDirs",
"strict",
"strictBindCallApply",
"strictBuiltinIteratorReturn",
"strictFunctionTypes",
"strictNullChecks",
"strictPropertyInitialization",
"types",
"useUnknownInCatchVariables",
"verbatimModuleSyntax",
];
41 changes: 0 additions & 41 deletions client/src/enable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,54 +4,13 @@ import { ENABLE, ENABLE_PATHS, EXTENSION_NS } from "./constants";

import * as vscode from "vscode";
import { DenoExtensionContext, EnableSettings } from "./types";
import * as os from "os";
import * as path from "path";

export interface WorkspaceEnabledInfo {
folder: vscode.WorkspaceFolder;
enabled: boolean | undefined;
hasDenoConfig: boolean;
}

const PARENT_RELATIVE_REGEX = os.platform() === "win32"
? /\.\.(?:[/\\]|$)/
: /\.\.(?:\/|$)/;

/** Checks if `parent` is an ancestor of `child`. */
function pathStartsWith(child: string, parent: string) {
if (path.isAbsolute(child) !== path.isAbsolute(parent)) {
return false;
}
const relative = path.relative(parent, child);
return !relative.match(PARENT_RELATIVE_REGEX);
}

export function isPathEnabled(
extensionContext: DenoExtensionContext,
filePath: string,
) {
const enableSettings =
extensionContext.enableSettingsByFolder?.find(([workspace, _]) =>
pathStartsWith(filePath, workspace)
)?.[1] ?? extensionContext.enableSettingsUnscoped ??
{ enable: null, enablePaths: null, disablePaths: [] };
const scopesWithDenoJson = extensionContext.scopesWithDenoJson ?? new Set();
for (const path of enableSettings.disablePaths) {
if (pathStartsWith(filePath, path)) {
return false;
}
}
if (enableSettings.enablePaths) {
return enableSettings.enablePaths.some((p) => pathStartsWith(filePath, p));
}
if (enableSettings.enable != null) {
return enableSettings.enable;
}
return [...scopesWithDenoJson].some((scope) =>
pathStartsWith(filePath, scope)
);
}

export async function getWorkspacesEnabledInfo() {
const result: WorkspaceEnabledInfo[] = [];
for (const folder of vscode.workspace.workspaceFolders ?? []) {
Expand Down