Skip to content
Open
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
12 changes: 11 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,15 @@
"files.associations": {
"iostream": "cpp"
},
"editor.formatOnSave": true
"editor.formatOnSave": true,
"[typescript]": {
"editor.codeActionsOnSave": {
"source.organizeImports": "never"
}
},
"[typescriptreact]": {
"editor.codeActionsOnSave": {
"source.organizeImports": "never"
}
}
}
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"license": "GPL-3.0-or-later",
"icon": "icon.png",
"publisher": "DivyanshuAgrawal",
"version": "2077.0.0",
"version": "2081.0.0",
"engines": {
"vscode": "^1.52.0"
},
Expand Down Expand Up @@ -70,7 +70,7 @@
"cph.general.saveLocation": {
"type": "string",
"default": "",
"description": "Location where generated .tcs and .bin files will be saved. Leave empty to save the file in the source file directory. Use this to clean up your folders."
"description": "Directory where new problem source files and their .prob metadata will be saved. Supports the ${group} placeholder (e.g. '${group}/'), which is replaced with the problem's group. Leave empty to save in the workspace root for sources and alongside sources in .cph for metadata."
},
"cph.general.timeOut": {
"type": "number",
Expand Down Expand Up @@ -360,7 +360,7 @@
},
"cph.general.retainWebviewContext": {
"type": "boolean",
"default": false,
"default": true,
"description": "Keep the webview active even when it's hidden. May improve performance but may cause some rendering issues."
},
"cph.general.defaultLanguageTemplateFileLocation": {
Expand Down
38 changes: 35 additions & 3 deletions src/companion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { Problem, CphSubmitResponse, CphEmptyResponse } from './types';
import { saveProblem } from './parser';
import * as vscode from 'vscode';
import path from 'path';
import { writeFileSync, readFileSync, existsSync } from 'fs';
import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs';
import { isCodeforcesUrl, isLuoguUrl, isAtCoderUrl, randomId } from './utils';
import {
getDefaultLangPref,
getSaveLocationPref,
getLanguageId,
useShortCodeForcesName,
useShortLuoguName,
Expand Down Expand Up @@ -221,7 +222,20 @@ const handleNewProblem = async (problem: Problem) => {
problem.name = splitUrl[splitUrl.length - 1];
}
const problemFileName = getProblemFileName(problem, extn);
const srcPath = path.join(folder, problemFileName);
let configuredSaveDir = getSaveLocationPref();
if (configuredSaveDir && configuredSaveDir.includes('${group}')) {
const groupKey = (problem.group || '').trim();
const replacement = groupKey === '' ? '' : groupKey;
configuredSaveDir = configuredSaveDir.replace(
/\$\{group\}/g,
replacement,
);
}
const targetDir =
configuredSaveDir && configuredSaveDir !== ''
? configuredSaveDir
: folder;
const srcPath = path.join(targetDir, problemFileName);

// Add fields absent in competitive companion.
problem.srcPath = srcPath;
Expand All @@ -230,11 +244,29 @@ const handleNewProblem = async (problem: Problem) => {
// Pass in index to avoid generating duplicate id
id: randomId(index),
}));
if (!existsSync(path.dirname(srcPath))) {
try {
// ensure nested paths if user configured nested folder
mkdirSync(path.dirname(srcPath), { recursive: true });
} catch (e) {
globalThis.logger.error('Failed to create target directory', e);
}
}
if (!existsSync(srcPath)) {
writeFileSync(srcPath, '');
}
saveProblem(srcPath, problem);
const doc = await vscode.workspace.openTextDocument(srcPath);
// Avoid redundant openTextDocument if already open
const visibleEditor = vscode.window.visibleTextEditors.find(
(e) => e.document.fileName === srcPath,
);
const existingDoc =
visibleEditor?.document ||
vscode.workspace.textDocuments.find((d) => d.fileName === srcPath);
const doc =
existingDoc !== undefined
? existingDoc
: await vscode.workspace.openTextDocument(srcPath);

if (defaultLanguage) {
const templateLocation = getDefaultLanguageTemplateFileLocation();
Expand Down
6 changes: 5 additions & 1 deletion src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ export const getBinSaveLocation = (srcPath: string): string => {
const srcFileName = path.parse(srcPath).name;
const binFileName = toAsciiFilename(srcFileName) + ext;
const binDir = path.dirname(srcPath);
if (savePreference && savePreference !== '') {
if (
savePreference &&
savePreference !== '' &&
!savePreference.includes('${')
) {
return path.join(savePreference, binFileName);
}
return path.join(binDir, binFileName);
Expand Down
123 changes: 105 additions & 18 deletions src/parser.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import path from 'path';
import fs from 'fs';
import * as vscode from 'vscode';
import { Problem } from './types';
import { getSaveLocationPref } from './preferences';
import crypto from 'crypto';

/**
* Get the location (file path) to save the generated problem file in. If save
Expand All @@ -12,30 +12,107 @@ import crypto from 'crypto';
* @param srcPath location of the source code
*/
export const getProbSaveLocation = (srcPath: string): string => {
const savePreference = getSaveLocationPref();
const srcFileName = path.basename(srcPath);
const srcFolder = path.dirname(srcPath);
const hash = crypto
.createHash('md5')
.update(srcPath)
.digest('hex')
.substr(0);
const baseProbName = `.${srcFileName}_${hash}.prob`;
const baseProbName = `${srcFileName}.prob`;
const cphFolder = path.join(srcFolder, '.cph');
if (savePreference && savePreference !== '') {
return path.join(savePreference, baseProbName);
}
return path.join(cphFolder, baseProbName);
};

/** Find the .prob path for the given source by scanning ancestor .cph folders. */
export const findProbPath = (srcPath: string): string | null => {
const srcFolder = path.dirname(srcPath);
const srcFileName = path.basename(srcPath);

const workspaceFolder = vscode.workspace.getWorkspaceFolder(
vscode.Uri.file(srcPath),
);
const workspaceRoot =
workspaceFolder?.uri.fsPath ?? path.parse(srcFolder).root;

const ancestors: string[] = [];
let currentDir = srcFolder;
// Collect ancestor directories up to workspace root (inclusive)
let reachedRoot = false;
while (!reachedRoot) {
ancestors.push(currentDir);
reachedRoot =
path.resolve(currentDir) === path.resolve(workspaceRoot) ||
path.dirname(currentDir) === currentDir;
if (!reachedRoot) {
currentDir = path.dirname(currentDir);
}
}

for (const dir of ancestors) {
const cphFolder = path.join(dir, '.cph');
if (!fs.existsSync(cphFolder)) {
continue;
}
const files = fs
.readdirSync(cphFolder)
.filter((f) => f.endsWith('.prob'));
if (files.length === 0) {
continue;
}
// Prioritize files that start with the source filename
const prioritized = [];
const nonPrioritized = [];
for (const file of files) {
const name = file.startsWith('.') ? file.slice(1) : file;
if (name.startsWith(srcFileName)) {
prioritized.push(file);
} else {
nonPrioritized.push(file);
}
}

const all = [...prioritized, ...nonPrioritized];

for (const file of all) {
const fullProbPath = path.join(cphFolder, file);
try {
const content = fs.readFileSync(fullProbPath).toString();
const parsed: Problem = JSON.parse(content);
const recorded = (parsed as any).srcPath as string | undefined;
if (!recorded) {
continue;
}
const parentOfCph = dir; // parent of the .cph folder
let resolvedRecorded: string;
if (path.isAbsolute(recorded)) {
resolvedRecorded = path.normalize(recorded);
} else {
resolvedRecorded = path.resolve(parentOfCph, recorded);
}
const samePath =
path.normalize(resolvedRecorded) ===
path.normalize(srcPath);
if (samePath) {
return fullProbPath;
}
} catch (_e) {
// Ignore invalid/partial files
continue;
}
}
}

return null;
};

/** Get the problem for a source, `null` if does not exist on the filesystem. */
export const getProblem = (srcPath: string): Problem | null => {
const probPath = getProbSaveLocation(srcPath);
let problem: string;
const probPath = findProbPath(srcPath);
if (!probPath) {
return null;
}
try {
problem = fs.readFileSync(probPath).toString();
return JSON.parse(problem);
} catch (err) {
const content = fs.readFileSync(probPath).toString();
const parsed: Problem = JSON.parse(content);
parsed.srcPath = srcPath;
return parsed;
} catch (_e) {
return null;
}
};
Expand All @@ -45,14 +122,24 @@ export const saveProblem = (srcPath: string, problem: Problem) => {
const srcFolder = path.dirname(srcPath);
const cphFolder = path.join(srcFolder, '.cph');

if (getSaveLocationPref() === '' && !fs.existsSync(cphFolder)) {
const pref = getSaveLocationPref();
if (pref === '' && !fs.existsSync(cphFolder)) {
globalThis.logger.log('Making .cph folder');
fs.mkdirSync(cphFolder);
}

const probPath = getProbSaveLocation(srcPath);
const probDir = path.dirname(probPath);
if (!fs.existsSync(probDir)) {
fs.mkdirSync(probDir, { recursive: true });
}
try {
fs.writeFileSync(probPath, JSON.stringify(problem));
const problemToSave: Problem = {
...problem,
// Store path relative to parent of .cph folder when possible
srcPath: path.relative(path.dirname(probDir), srcPath),
};
fs.writeFileSync(probPath, JSON.stringify(problemToSave, null, 2));
} catch (err) {
throw new Error(err as string);
}
Expand Down
35 changes: 25 additions & 10 deletions src/preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,35 @@ export const getAutoShowJudgePref = (): boolean =>
getPreference('general.autoShowJudge');

export const getSaveLocationPref = (): string => {
const pref = getPreference('general.saveLocation');
const validSaveLocation = pref == '' || fs.existsSync(pref);
if (!validSaveLocation) {
const raw = getPreference('general.saveLocation') as string;
if (raw === '') return '';

let resolved = raw;
if (!path.isAbsolute(raw)) {
const folder = workspace.workspaceFolders?.[0]?.uri.fsPath;
if (!folder) {
vscode.window.showErrorMessage(
`Save location '${raw}' is relative but no workspace is open.`,
);
return '';
}
resolved = path.join(folder, raw);
}

try {
// Do not create directories if path contains template placeholders like ${group}
if (!resolved.includes('${')) {
if (!fs.existsSync(resolved)) {
fs.mkdirSync(resolved, { recursive: true });
}
}
return resolved;
} catch (e) {
vscode.window.showErrorMessage(
`Invalid save location, reverting to default. path not exists: ${pref}`,
);
updatePreference(
'general.saveLocation',
'',
vscode.ConfigurationTarget.Global,
`Could not create save location: ${resolved}. Falling back to default. ${e}`,
);
return '';
}
return pref;
};

export const getHideStderrorWhenCompiledOK = (): boolean =>
Expand Down
20 changes: 17 additions & 3 deletions src/runTestCases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,17 @@ export default async () => {
const problem = getProblem(srcPath);

if (!problem) {
globalThis.logger.log('No problem saved.');
createLocalProblem(editor);
const judge = getJudgeViewProvider();
if (judge.isOpen()) {
await createLocalProblem();
} else {
globalThis.logger.log('No problem saved. Showing create UI.');
judge.focus();
judge.extensionToJudgeViewMessage({
command: 'new-problem',
problem: undefined,
});
}
return;
}

Expand All @@ -51,9 +60,14 @@ export default async () => {
vscode.window.showTextDocument(editor.document, vscode.ViewColumn.One);
};

const createLocalProblem = async (editor: vscode.TextEditor) => {
export const createLocalProblem = async () => {
globalThis.reporter.sendTelemetryEvent(telmetry.NEW_LOCAL_PROBLEM);
globalThis.logger.log('Creating local problem');
const editor = vscode.window.activeTextEditor;
if (editor === undefined) {
checkUnsupported('');
return;
}
const srcPath = editor.document.fileName;
if (checkUnsupported(srcPath)) {
return;
Expand Down
Loading