Skip to content
Open
1 change: 1 addition & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
MIT License

Copyright (c) 2021 Sam Gwilym
Copyright (c) 2015-present, Brian Woodward

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
39 changes: 38 additions & 1 deletion deno.novaextension/extension.json
Original file line number Diff line number Diff line change
Expand Up @@ -142,12 +142,12 @@
{
"id": "co.gwil.deno.sidebars.symbols",
"name": "Deno Symbols",
"placeholderText": "Use the Find Symbol command to find symbols with Deno.",
"largeImage": "sidebar_large",
"smallImage": "sidebar_small",
"sections": [
{
"id": "co.gwil.deno.sidebars.symbols.sections.1",
"placeholderText": "Use the Find Symbol command to find symbols with Deno.",
"name": "Symbols",
"headerCommands": [
{
Expand All @@ -157,6 +157,43 @@
]
}
]
},
{
"id": "co.gwil.deno.sidebars.tests",
"name": "Deno Tests",
"largeImage": "test_sidebar_large",
"smallImage": "test_sidebar_small",
"sections": [
{
"id": "co.gwil.deno.sidebars.tests.sections.1",
"placeholderText": "No tests were found.",
"name": "Tests",
"headerCommands": [
{
"title": "Learn more",
"command": "co.gwil.deno.sidebars.tests.commands.learn"
},
{
"title": "Refresh",
"tooltip": "Check for the addition or removal of test files",
"command": "co.gwil.deno.sidebars.tests.commands.refresh",
"image": "__builtin.refresh"
},
{
"title": "Run All",
"command": "co.gwil.deno.sidebars.tests.commands.runAll",
"image": "run"
}
],
"contextCommands": [
{
"title": "Run",
"command": "co.gwil.deno.sidebars.tests.commands.run",
"when": "viewItem == 'file'"
}
]
}
]
}
],

Expand Down
9 changes: 2 additions & 7 deletions src/client_disposable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import {
import registerFormatDocument from "./commands/format_document.ts";
import registerCache from "./commands/cache.ts";
import registerRenameSymbol from "./commands/rename_symbol.ts";
import registerPaletteFindSymbol from "./commands/palette_find_symbol.ts";
import registerSymbolSidebarFindSymbol from "./commands/sidebar_find_symbol.ts";
import registerFindSymbol from "./find_symbol/register.ts";
import syntaxes from "./syntaxes.ts";

const FORMAT_ON_SAVE_CONFIG_KEY = "co.gwil.deno.config.formatOnSave";
Expand Down Expand Up @@ -140,11 +139,7 @@ export async function makeClientDisposable(
clientDisposable.add(registerFormatDocument(client));
clientDisposable.add(registerCache(client));
clientDisposable.add(registerRenameSymbol(client));

// palette Find Symbol command
clientDisposable.add(registerPaletteFindSymbol());
// sidebar Find Symbol command
clientDisposable.add(registerSymbolSidebarFindSymbol(client));
clientDisposable.add(registerFindSymbol(client));

nova.workspace.onDidAddTextEditor((editor) => {
const editorDisposable = new CompositeDisposable();
Expand Down
26 changes: 0 additions & 26 deletions src/commands/palette_find_symbol.ts

This file was deleted.

81 changes: 81 additions & 0 deletions src/commands/symbols.sidebar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {
LanguageClient,
NotificationRequest,
nova,
wrapCommand,
} from "../nova_utils.ts";
import SymbolDataProvider from "../find_symbol/SymbolDataProvider.ts";
import { lsp } from "../../deps.ts";

export function registerPaletteFindSymbol() {
return nova.commands.register(
"co.gwil.deno.commands.find",
wrapCommand(maybeFindSymbol),
);

async function maybeFindSymbol() {
try {
await nova.commands.invoke("co.gwil.deno.sidebars.symbols.commands.find");
} catch (err) {
// I don't think this ever happens.
console.error(err);
}

const sidebarReminderNotificationRequest = new NotificationRequest(
"co.gwil.deno.notifications.checkTheSidebar",
);
sidebarReminderNotificationRequest.title =
"Symbols are shown in the Deno Symbols sidebar.";
sidebarReminderNotificationRequest.body =
"To see your search's results, check the Deno Symbols sidebar.";
nova.notifications.add(sidebarReminderNotificationRequest);
}
}

export function registerSidebarFindSymbol(
client: LanguageClient,
symbolDataProvider: SymbolDataProvider,
) {
return nova.commands.register(
"co.gwil.deno.sidebars.symbols.commands.find",
wrapCommand(findSymbol),
);

async function findSymbol() {
if (
// @ts-expect-error: The Nova types are outdated.
!(nova.workspace.context as Configuration).get("shouldDisplayFeatures")
) {
const failureNotificationReq = new NotificationRequest(
"co.gwil.deno.notifications.findSymbolUnavailable",
);
failureNotificationReq.title = "Find Symbol is unavailable.";
failureNotificationReq.body =
"Open a TypeScript, JavaScript, JSX or TSX file.";
nova.notifications.add(failureNotificationReq);
return;
}

const query = await new Promise((resolve) =>
nova.workspace.showInputPalette(
"Type the name of a variable, class or function.",
{},
resolve,
)
) as string | null | undefined;

// This happens if the user exits the palette, for example, by pressing Escape.
if (!query) return;

symbolDataProvider.displaySymbols(query, getSymbols);

async function getSymbols(query: string) {
const params = { query };
const response = await client.sendRequest(
"workspace/symbol",
params,
) as lsp.SymbolInformation[] | null;
return response;
}
}
}
176 changes: 176 additions & 0 deletions src/commands/tests.sidebar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import {
FileTextMode,
NotificationRequest,
nova,
Process,
wrapCommand,
} from "../nova_utils.ts";
import TestsDataProvider, {
TestFile,
UnexpectedLogError,
} from "../tests/TestsDataProvider.ts";

export function registerLearnMore() {
return nova.commands.register(
"co.gwil.deno.sidebars.tests.commands.learn",
wrapCommand(learnMore),
);

function learnMore() {
const options = {
args: ["https://deno.land/manual/testing#running-tests"],
};
const process = new Process("/usr/bin/open", options);
process.start();
}
}

export function registerRefresh(testsDataProvider: TestsDataProvider) {
return nova.commands.register(
"co.gwil.deno.sidebars.tests.commands.refresh",
wrapCommand(refresh),
);

function refresh() {
if (nova.workspace.path) {
testsDataProvider.updateFiles(nova.workspace.path);
testsDataProvider.treeView.reload();
} else {
const informativeNotificationRequest = new NotificationRequest(
"co.gwil.deno.notifications.missingWorkspacePathForTests",
);
informativeNotificationRequest.title =
"The tests sidebar is unavailable.";
informativeNotificationRequest.body =
"Open a project in which to look for tests.";
nova.notifications.add(informativeNotificationRequest);
}
}
}

export function registerRunAll(testsDataProvider: TestsDataProvider) {
return nova.commands.register(
"co.gwil.deno.sidebars.tests.commands.runAll",
wrapCommand(runAll),
);

async function runAll() {
const hiddenWorkspaceDataPath = nova.path.join(
nova.extension.workspaceStoragePath,
"state.json",
);

function readState(path: string): Record<string, unknown> {
try {
const file = nova.fs.open(path) as FileTextMode;
const content = file.readlines().join("\n");
file.close();

return JSON.parse(content);
} catch {
return {};
}
}
function writeState(path: string, content: Record<string, unknown>) {
let file;
try {
file = nova.fs.open(path, "w") as FileTextMode;
} catch {
try {
nova.fs.mkdir(nova.extension.workspaceStoragePath);
} catch (e) {
throw new Error(
"Could not access the extension's workspace storage path.",
{ cause: e },
);
}
file = nova.fs.open(path, "w") as FileTextMode;
}
file.write(JSON.stringify(content));
}

const state = readState(hiddenWorkspaceDataPath);
if (!state.warningWasShown) {
const permissionsNotificationRequest = new NotificationRequest(
"co.gwil.deno.notifications.findSymbolUnavailable",
);
permissionsNotificationRequest.title =
"Tests are awarded all permissions.";
permissionsNotificationRequest.body =
"Test files may access environment variables, load dynamic libraries, measure time in high resolution, utilize the network, read files and write files. This is not configurable at the moment.";
permissionsNotificationRequest.actions = ["Cancel", "Allow"];

const response = await nova.notifications.add(
permissionsNotificationRequest,
);
if (response.actionIdx == 0) {
return;
}

state.warningWasShown = true;
writeState(hiddenWorkspaceDataPath, state);
}

const timeoutID = setTimeout(() => {
const stillRunningNotificationRequest = new NotificationRequest(
"co.gwil.deno.notifications.runningTests",
);
stillRunningNotificationRequest.title = "The tests are still being run.";
stillRunningNotificationRequest.body =
"It's just taking a while. Please wait.";
nova.notifications.add(stillRunningNotificationRequest);
}, 3 * 1000);

try {
await testsDataProvider.runTests();
testsDataProvider.treeView.reload();
} catch (e) {
if (e instanceof UnexpectedLogError) {
const dissatisfactoryOutcomeNotificationRequest =
new NotificationRequest(
"co.gwil.deno.notifications.fragileTestsSidebar",
);
dissatisfactoryOutcomeNotificationRequest.title =
"The tests' outcomes are unknown.";
dissatisfactoryOutcomeNotificationRequest.body =
"This feature is fragile, and relies on the extension's compatibility with your particular Deno version. If you're using an outdated version of Deno, or an outdated version of the extension, try updating.";
dissatisfactoryOutcomeNotificationRequest.actions = ["OK"];
nova.notifications.add(dissatisfactoryOutcomeNotificationRequest);
} else {
// shown as a Nova message regardless; doesn't produce a crash
throw e;
}
} finally {
clearTimeout(timeoutID);
}
}
}

export function registerRun(testsDataProvider: TestsDataProvider) {
return nova.commands.register(
"co.gwil.deno.sidebars.tests.commands.run",
wrapCommand(run),
);

async function run() {
// This command is only available when `TestFile`s, exclusively, are selected.
const selected = testsDataProvider.treeView.selection as TestFile[];
await testsDataProvider.runTests(selected.map((file) => file.path));
testsDataProvider.treeView.reload();
}
}

// This wrapper is used by the sidebar to enable test files to be opened by double-clicking.
export function registerOpen(testsDataProvider: TestsDataProvider) {
return nova.commands.register(
"co.gwil.deno.sidebars.tests.commands.open",
wrapCommand(open),
);

function open() {
const selected = testsDataProvider.treeView.selection[0];
if (selected?.path) {
nova.workspace.openFile(selected.path);
}
}
}
Loading