diff --git a/src/api/index.ts b/src/api/index.ts index 14ae1090..b3342f04 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -111,7 +111,7 @@ export class AtelierAPI { if ( parts.length === 2 && (config("intersystems.servers").has(parts[0].toLowerCase()) || - vscode.workspace.workspaceFolders.find( + vscode.workspace.workspaceFolders?.find( (ws) => ws.uri.scheme === "file" && ws.name.toLowerCase() === parts[0].toLowerCase() )) ) { diff --git a/src/commands/compile.ts b/src/commands/compile.ts index cebe6ef8..a1d17c9b 100644 --- a/src/commands/compile.ts +++ b/src/commands/compile.ts @@ -92,6 +92,7 @@ export async function importFile( ignoreConflict?: boolean, skipDeplCheck = false ): Promise { + if (!file) return; const api = new AtelierAPI(file.uri); if (!api.active) return; if (file.name.split(".").pop().toLowerCase() === "cls" && !skipDeplCheck) { @@ -261,6 +262,8 @@ export async function loadChanges(files: (CurrentTextFile | CurrentBinaryFile)[] } export async function compile(docs: (CurrentTextFile | CurrentBinaryFile)[], flags?: string): Promise { + docs = docs.filter(notNull); + if (!docs.length) return; const wsFolder = vscode.workspace.getWorkspaceFolder(docs[0].uri); const conf = vscode.workspace.getConfiguration("objectscript", wsFolder || docs[0].uri); flags = flags || conf.get("compileFlags"); @@ -379,9 +382,7 @@ export async function compileOnly(askFlags = false, document?: vscode.TextDocume export async function namespaceCompile(askFlags = false): Promise { const api = new AtelierAPI(); const fileTypes = ["*.CLS", "*.MAC", "*.INC", "*.BAS"]; - if (!config("conn").active) { - throw new Error(`No Active Connection`); - } + if (!api.active) return; const confirm = await vscode.window.showWarningMessage( `Compiling all files in namespace ${api.ns} might be expensive. Are you sure you want to proceed?`, "Cancel", @@ -437,18 +438,20 @@ async function importFiles(files: vscode.Uri[], noCompile = false) { rateLimiter.call(async () => { return vscode.workspace.fs .readFile(uri) - .then((contentBytes) => { - if (isText(uri.path.split("/").pop(), Buffer.from(contentBytes))) { - const textFile = currentFileFromContent(uri, new TextDecoder().decode(contentBytes)); - toCompile.push(textFile); - return textFile; - } else { - return currentFileFromContent(uri, Buffer.from(contentBytes)); + .then((contentBytes) => + currentFileFromContent( + uri, + isText(uri.path.split("/").pop(), Buffer.from(contentBytes)) + ? new TextDecoder().decode(contentBytes) + : Buffer.from(contentBytes) + ) + ) + .then((curFile) => { + if (curFile) { + if (typeof curFile.content == "string") toCompile.push(curFile); // Only compile text files + return importFile(curFile).then(() => outputChannel.appendLine("Imported file: " + curFile.fileName)); } - }) - .then((curFile) => - importFile(curFile).then(() => outputChannel.appendLine("Imported file: " + curFile.fileName)) - ); + }); }) ) ); @@ -460,6 +463,7 @@ async function importFiles(files: vscode.Uri[], noCompile = false) { } export async function importFolder(uri: vscode.Uri, noCompile = false): Promise { + if (!(uri instanceof vscode.Uri)) return; if (filesystemSchemas.includes(uri.scheme)) return; // Not for server-side URIs if ((await vscode.workspace.fs.stat(uri)).type != vscode.FileType.Directory) { return importFiles([uri], noCompile); diff --git a/src/commands/connectFolderToServerNamespace.ts b/src/commands/connectFolderToServerNamespace.ts index 3b764477..6b40ffb5 100644 --- a/src/commands/connectFolderToServerNamespace.ts +++ b/src/commands/connectFolderToServerNamespace.ts @@ -42,6 +42,7 @@ export async function connectFolderToServerNamespace(): Promise { items.length === 1 && !items[0].detail ? items[0] : await vscode.window.showQuickPick(items, { title: "Pick a folder" }); + if (!pick) return; const folder = vscode.workspace.workspaceFolders.find((el) => el.name === pick.label); // Get user's choice of server const options: vscode.QuickPickOptions = {}; diff --git a/src/commands/studio.ts b/src/commands/studio.ts index 6fb102b6..2eae35f4 100644 --- a/src/commands/studio.ts +++ b/src/commands/studio.ts @@ -1,7 +1,7 @@ import * as vscode from "vscode"; import { AtelierAPI } from "../api"; import { iscIcon } from "../extension"; -import { outputChannel, outputConsole, notIsfs, handleError, openLowCodeEditors } from "../utils"; +import { outputChannel, outputConsole, notIsfs, handleError, openLowCodeEditors, stringifyError } from "../utils"; import { DocumentContentProvider } from "../providers/DocumentContentProvider"; import { UserAction } from "../api/atelier"; import { isfsDocumentName } from "../providers/FileSystemProvider/FileSystemProvider"; @@ -113,7 +113,7 @@ export class StudioActions { ); } - public processUserAction(userAction: UserAction): Thenable { + public async processUserAction(userAction: UserAction): Promise { const serverAction = userAction.action; const { target, errorText } = userAction; if (errorText !== "") { @@ -128,7 +128,22 @@ export class StudioActions { return vscode.window .showWarningMessage(target, { modal: true }, "Yes", "No") .then((answer) => (answer === "Yes" ? "1" : answer === "No" ? "0" : "2")); - case 2: // Run a CSP page/Template. The Target is the full path of CSP page/template on the connected server + case 2: { + // Run a CSP page/Template. The Target is the full path of CSP page/template on the connected server + + // Do this ourself instead of using our new getCSPToken wrapper function, because that function reuses tokens which causes issues with + // webview when server is 2020.1.1 or greater, as session cookie scope is typically Strict, meaning that the webview + // cannot store the cookie. Consequence is web sessions may build up (they get a 900 second timeout) + const cspchd = await this.api + .actionQuery("select %Atelier_v1_Utils.General_GetCSPToken(?) token", [target]) + .then((data) => data.result.content[0].token) + .catch((error) => { + outputChannel.appendLine( + `Failed to construct a CSP session for server-side source control User Action 2 (show a CSP page) on page '${target}': ${stringifyError(error)}\nReturning answer 2 (Cancel)` + ); + outputChannel.show(true); + }); + if (!cspchd) return "2"; return new Promise((resolve) => { let answer = "2"; const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined; @@ -157,16 +172,10 @@ export class StudioActions { const url = new URL( `${config.https ? "https" : "http"}://${config.host}:${config.port}${config.pathPrefix}${target}` ); - - // Do this ourself instead of using our new getCSPToken wrapper function, because that function reuses tokens which causes issues with - // webview when server is 2020.1.1 or greater, as session cookie scope is typically Strict, meaning that the webview - // cannot store the cookie. Consequence is web sessions may build up (they get a 900 second timeout) - this.api.actionQuery("select %Atelier_v1_Utils.General_GetCSPToken(?) token", [target]).then((tokenObj) => { - const csptoken = tokenObj.result.content[0].token; - url.searchParams.set("CSPCHD", csptoken); - url.searchParams.set("CSPSHARE", "1"); - url.searchParams.set("Namespace", this.api.config.ns); - panel.webview.html = ` + url.searchParams.set("CSPCHD", cspchd); + url.searchParams.set("CSPSHARE", "1"); + url.searchParams.set("Namespace", this.api.ns); + panel.webview.html = ` @@ -194,18 +203,21 @@ export class StudioActions { `; - }); }); + } case 3: { // Run an EXE on the client. - const urlRegex = /^(ht|f)tp(s?):\/\//gim; - if (target.search(urlRegex) === 0) { + if (/^(ht|f)tps?:\/\//i.test(target)) { // Allow target that is a URL to be opened in an external browser vscode.env.openExternal(vscode.Uri.parse(target)); - break; } else { - throw new Error("processUserAction: Run EXE (Action=3) not supported"); + // Anything else is not supported + outputChannel.appendLine( + `Server-side source control User Action 3 (run an EXE on the client) is not supported for target '${target}'` + ); + outputChannel.show(true); } + break; } case 4: { // Insert the text in Target in the current document at the current selection point @@ -219,39 +231,45 @@ export class StudioActions { } case 5: // Studio will open the documents listed in Target target.split(",").forEach((element) => { - let classname: string = element; + let fileName: string = element; let method: string; let offset = 0; if (element.includes(":")) { - [classname, method] = element.split(":"); + [fileName, method] = element.split(":"); if (method.includes("+")) { offset = +method.split("+")[1]; method = method.split("+")[0]; } } - const splitClassname = classname.split("."); - const filetype = splitClassname[splitClassname.length - 1]; + const fileExt = fileName.split(".").pop().toLowerCase(); const isCorrectMethod = (text: string) => - filetype === "cls" ? text.match("Method " + method) : text.startsWith(method); - - const uri = DocumentContentProvider.getUri(classname); - vscode.window.showTextDocument(uri, { preview: false }).then((newEditor) => { - if (method) { - const document = newEditor.document; - for (let i = 0; i < document.lineCount; i++) { - const line = document.lineAt(i); - if (isCorrectMethod(line.text)) { - if (!line.text.endsWith("{")) offset++; - const targetLine = document.lineAt(i + offset); - const range = new vscode.Range(targetLine.range.start, targetLine.range.start); - newEditor.selection = new vscode.Selection(range.start, range.start); - newEditor.revealRange(range, vscode.TextEditorRevealType.InCenter); - break; + fileExt === "cls" ? text.match("Method " + method) : text.startsWith(method); + + vscode.window.showTextDocument(DocumentContentProvider.getUri(fileName), { preview: false }).then( + (newEditor) => { + if (method) { + const document = newEditor.document; + for (let i = 0; i < document.lineCount; i++) { + const line = document.lineAt(i); + if (isCorrectMethod(line.text)) { + if (!line.text.endsWith("{")) offset++; + const targetLine = document.lineAt(i + offset); + const range = new vscode.Range(targetLine.range.start, targetLine.range.start); + newEditor.selection = new vscode.Selection(range.start, range.start); + newEditor.revealRange(range, vscode.TextEditorRevealType.InCenter); + break; + } } } + }, + (error) => { + outputChannel.appendLine( + `Server-side source control User Action 5 failed to show '${element}': ${stringifyError(error)}` + ); + outputChannel.show(true); } - }); + ); }); break; case 6: // Display an alert dialog in Studio with the text from the Target variable. @@ -268,7 +286,8 @@ export class StudioActions { }; }); default: - throw new Error(`processUserAction: ${userAction} not supported`); + outputChannel.appendLine(`Unknown server-side source control User Action ${serverAction} is not supported`); + outputChannel.show(true); } return Promise.resolve(); } diff --git a/src/commands/viewOthers.ts b/src/commands/viewOthers.ts index 77973ab9..fdce801b 100644 --- a/src/commands/viewOthers.ts +++ b/src/commands/viewOthers.ts @@ -53,7 +53,7 @@ export async function viewOthers(forceEditable = false): Promise { const methodlinetext: string = doc.lineAt(methodlinenum).text.trim(); if (methodlinetext.endsWith("{")) { // This is the last line of the method definition, so count from here - const selectionline: number = methodlinenum + +loc.slice(loc.lastIndexOf("+") + 1); + const selectionline: number = methodlinenum + (+loc.slice(loc.lastIndexOf("+") + 1) || 0); options.selection = new vscode.Range(selectionline, 0, selectionline, 0); break; } @@ -68,7 +68,7 @@ export async function viewOthers(forceEditable = false): Promise { loc = loc.slice(0, loc.lastIndexOf("+")); } // Locations in INT routines are of the format +offset - const linenum: number = +loc.slice(1); + const linenum: number = +loc.slice(1) || 0; options.selection = new vscode.Range(linenum, 0, linenum, 0); } vscode.window.showTextDocument(uri, options); diff --git a/src/debug/debugSession.ts b/src/debug/debugSession.ts index 525401cd..8e592504 100644 --- a/src/debug/debugSession.ts +++ b/src/debug/debugSession.ts @@ -313,29 +313,37 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { response: DebugProtocol.PauseResponse, args: DebugProtocol.PauseArguments ): Promise { - const xdebugResponse = await this._connection.sendBreakCommand(); - this.sendResponse(response); - await this._checkStatus(xdebugResponse); + try { + const xdebugResponse = await this._connection.sendBreakCommand(); + this.sendResponse(response); + this._checkStatus(xdebugResponse); + } catch (error) { + this.sendErrorResponse(response, error); + } } protected async configurationDoneRequest( response: DebugProtocol.ConfigurationDoneResponse, args: DebugProtocol.ConfigurationDoneArguments ): Promise { - if (!this._isLaunch && !this._isCsp) { - // The debug agent ignores the first run command - // for non-CSP attaches, so send one right away - await this._connection.sendRunCommand(); - // Tell VS Code that we're stopped - this.sendResponse(response); - const event: DebugProtocol.StoppedEvent = new StoppedEvent("entry", this._connection.id); - event.body.allThreadsStopped = false; - this.sendEvent(event); - } else { - // Tell the debugger to run the target process - const xdebugResponse = await this._connection.sendRunCommand(); - this.sendResponse(response); - await this._checkStatus(xdebugResponse); + try { + if (!this._isLaunch && !this._isCsp) { + // The debug agent ignores the first run command + // for non-CSP attaches, so send one right away + await this._connection.sendRunCommand(); + // Tell VS Code that we're stopped + this.sendResponse(response); + const event: DebugProtocol.StoppedEvent = new StoppedEvent("entry", this._connection.id); + event.body.allThreadsStopped = false; + this.sendEvent(event); + } else { + // Tell the debugger to run the target process + const xdebugResponse = await this._connection.sendRunCommand(); + this.sendResponse(response); + this._checkStatus(xdebugResponse); + } + } catch (error) { + this.sendErrorResponse(response, error); } } @@ -348,9 +356,13 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { // Detach is always supported by the debug agent // If attach, it will detach from the target // If launch, it will terminate the target - const xdebugResponse = await this._connection.sendDetachCommand(); - this.sendResponse(response); - await this._checkStatus(xdebugResponse); + try { + const xdebugResponse = await this._connection.sendDetachCommand(); + this.sendResponse(response); + this._checkStatus(xdebugResponse); + } catch (error) { + this.sendErrorResponse(response, error); + } } else { this.sendResponse(response); } @@ -648,207 +660,216 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments ): Promise { - const stack = await this._connection.sendStackGetCommand(); - - /** Is set to true if we're at the CSP or unit test ending watchpoint. - * We need to do this so VS Code doesn't try to open the source of - * a stack frame before the debug session terminates. */ - let noStack = false; - const stackFrames = await Promise.all( - stack.stack.map(async (stackFrame: xdebug.StackFrame, index): Promise => { - if (noStack) return; // Stack frames won't be sent - const [, namespace, docName] = decodeURI(stackFrame.fileUri).match(/^dbgp:\/\/\|([^|]+)\|(.*)$/); - const fileUri = DocumentContentProvider.getUri( - docName, - this._workspace, - namespace, - undefined, - this._workspaceFolderUri - ); - const source = new Source(docName, fileUri.toString()); - let line = stackFrame.line + 1; - const place = `${stackFrame.method}+${stackFrame.methodOffset}`; - const stackFrameId = this._stackFrameIdCounter++; - if (index == 0 && this._break) { - const csp = this._isCsp && ["%SYS.cspServer.mac", "%SYS.cspServer.int"].includes(source.name); - const unitTest = this._isUnitTest && source.name.startsWith("%Api.Atelier.v"); - if (csp || unitTest) { - // Check if we're at our special watchpoint - const { result } = await this._connection.sendEvalCommand( - csp ? this._cspWatchpointCondition : this._unitTestWatchpointCondition - ); - if (result.type == "int" && result.value == "1") { - // Stop the debugging session - const xdebugResponse = await this._connection.sendDetachCommand(); - await this._checkStatus(xdebugResponse); - noStack = true; - return; + try { + const stack = await this._connection.sendStackGetCommand(); + + /** Is set to true if we're at the CSP or unit test ending watchpoint. + * We need to do this so VS Code doesn't try to open the source of + * a stack frame before the debug session terminates. */ + let noStack = false; + const stackFrames = await Promise.all( + stack.stack.map(async (stackFrame: xdebug.StackFrame, index): Promise => { + if (noStack) return; // Stack frames won't be sent + const [, namespace, docName] = decodeURI(stackFrame.fileUri).match(/^dbgp:\/\/\|([^|]+)\|(.*)$/); + const fileUri = DocumentContentProvider.getUri( + docName, + this._workspace, + namespace, + undefined, + this._workspaceFolderUri + ); + const source = new Source(docName, fileUri.toString()); + let line = stackFrame.line + 1; + const place = `${stackFrame.method}+${stackFrame.methodOffset}`; + const stackFrameId = this._stackFrameIdCounter++; + if (index == 0 && this._break) { + const csp = this._isCsp && ["%SYS.cspServer.mac", "%SYS.cspServer.int"].includes(source.name); + const unitTest = this._isUnitTest && source.name.startsWith("%Api.Atelier.v"); + if (csp || unitTest) { + // Check if we're at our special watchpoint + const { result } = await this._connection.sendEvalCommand( + csp ? this._cspWatchpointCondition : this._unitTestWatchpointCondition + ); + if (result.type == "int" && result.value == "1") { + // Stop the debugging session + const xdebugResponse = await this._connection.sendDetachCommand(); + this._checkStatus(xdebugResponse); + noStack = true; + return; + } } } - } - const fileText = await this._getFileText(fileUri); - const hasCmdLoc = typeof stackFrame.cmdBeginLine == "number"; - if (!fileText.length) { - // Can't get the source for the document - this._stackFrames.set(stackFrameId, stackFrame); + const fileText = await this._getFileText(fileUri); + const hasCmdLoc = typeof stackFrame.cmdBeginLine == "number"; + if (!fileText.length) { + // Can't get the source for the document + this._stackFrames.set(stackFrameId, stackFrame); + return { + id: stackFrameId, + name: place, + // Don't provide a source path so VS Code doesn't attempt + // to open this file or provide an option to "create" it + source: { + name: docName, + presentationHint: "deemphasize", + }, + line, + column: 0, + }; + } + let noSource = false; + try { + if (source.name.endsWith(".cls") && stackFrame.method !== "") { + // Compute DocumentSymbols for this class + const symbols: vscode.DocumentSymbol[] = ( + await vscode.commands.executeCommand( + "vscode.executeDocumentSymbolProvider", + fileUri + ) + )[0].children; + const newLine = methodOffsetToLine(symbols, fileText, stackFrame.method, stackFrame.methodOffset); + if (newLine != undefined) line = newLine; + } + this._stackFrames.set(stackFrameId, stackFrame); + } catch { + noSource = true; + } + const lineDiff = line - stackFrame.line; return { id: stackFrameId, name: place, - // Don't provide a source path so VS Code doesn't attempt - // to open this file or provide an option to "create" it - source: { - name: docName, - presentationHint: "deemphasize", - }, + source: noSource ? null : source, line, - column: 0, + column: hasCmdLoc ? stackFrame.cmdBeginPos + 1 : 0, + endLine: hasCmdLoc ? stackFrame.cmdEndLine + lineDiff : undefined, + endColumn: hasCmdLoc + ? (stackFrame.cmdEndPos == 0 + ? // A command that ends at position zero means "rest of this line" + fileText.split(/\r?\n/)[stackFrame.cmdEndLine + lineDiff - 1].length + : stackFrame.cmdEndPos) + 1 + : undefined, }; - } - let noSource = false; - try { - if (source.name.endsWith(".cls") && stackFrame.method !== "") { - // Compute DocumentSymbols for this class - const symbols: vscode.DocumentSymbol[] = ( - await vscode.commands.executeCommand( - "vscode.executeDocumentSymbolProvider", - fileUri - ) - )[0].children; - const newLine = methodOffsetToLine(symbols, fileText, stackFrame.method, stackFrame.methodOffset); - if (newLine != undefined) line = newLine; - } - this._stackFrames.set(stackFrameId, stackFrame); - } catch { - noSource = true; - } - const lineDiff = line - stackFrame.line; - return { - id: stackFrameId, - name: place, - source: noSource ? null : source, - line, - column: hasCmdLoc ? stackFrame.cmdBeginPos + 1 : 0, - endLine: hasCmdLoc ? stackFrame.cmdEndLine + lineDiff : undefined, - endColumn: hasCmdLoc - ? (stackFrame.cmdEndPos == 0 - ? // A command that ends at position zero means "rest of this line" - fileText.split(/\r?\n/)[stackFrame.cmdEndLine + lineDiff - 1].length - : stackFrame.cmdEndPos) + 1 - : undefined, - }; - }) - ); + }) + ); - this._break = false; - if (!noStack) { - response.body = { - stackFrames, - }; + this._break = false; + if (!noStack) { + response.body = { + stackFrames, + }; + } + this.sendResponse(response); + } catch (error) { + this.sendErrorResponse(response, error); } - this.sendResponse(response); } protected async scopesRequest( response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments ): Promise { - let scopes = new Array(); - const stackFrame = this._stackFrames.get(args.frameId); - if (!stackFrame) { - throw new Error(`Unknown frameId ${args.frameId}`); - } - const contexts = await stackFrame.getContexts(); - scopes = contexts.map((context) => { - const variableId = this._variableIdCounter++; - this._contexts.set(variableId, context); - if (context.id < this._contextNames.length) { - return new Scope(this._contextNames[context.id], variableId); - } else { - return new Scope(context.name, variableId); + try { + let scopes = new Array(); + const stackFrame = this._stackFrames.get(args.frameId); + if (!stackFrame) { + throw new Error(`Unknown frameId ${args.frameId}`); } - }); - response.body = { - scopes, - }; - this.sendResponse(response); + const contexts = await stackFrame.getContexts(); + scopes = contexts.map((context) => { + const variableId = this._variableIdCounter++; + this._contexts.set(variableId, context); + if (context.id < this._contextNames.length) { + return new Scope(this._contextNames[context.id], variableId); + } else { + return new Scope(context.name, variableId); + } + }); + response.body = { + scopes, + }; + this.sendResponse(response); + } catch (error) { + this.sendErrorResponse(response, error); + } } protected async variablesRequest( response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments ): Promise { - const variablesReference = args.variablesReference; - let variables = new Array(); - - let properties: xdebug.BaseProperty[]; - if (this._contexts.has(variablesReference)) { - // VS Code is requesting the variables for a SCOPE, so we have to do a context_get - const context = this._contexts.get(variablesReference); - properties = await context.getProperties(); - } else if (this._properties.has(variablesReference)) { - // VS Code is requesting the subelements for a variable, so we have to do a property_get - const property = this._properties.get(variablesReference); - if (property.hasChildren) { - if (property.children.length === property.numberOfChildren) { - properties = property.children; + try { + const variablesReference = args.variablesReference; + let variables = new Array(); + + let properties: xdebug.BaseProperty[]; + if (this._contexts.has(variablesReference)) { + // VS Code is requesting the variables for a SCOPE, so we have to do a context_get + const context = this._contexts.get(variablesReference); + properties = await context.getProperties(); + } else if (this._properties.has(variablesReference)) { + // VS Code is requesting the subelements for a variable, so we have to do a property_get + const property = this._properties.get(variablesReference); + if (property.hasChildren) { + if (property.children.length === property.numberOfChildren) { + properties = property.children; + } else { + properties = await property.getChildren(); + } } else { - properties = await property.getChildren(); + properties = []; } + } else if (this._evalResultProperties.has(variablesReference)) { + // the children of properties returned from an eval command are always inlined, so we simply resolve them + const property = this._evalResultProperties.get(variablesReference); + properties = property.hasChildren ? property.children : []; } else { - properties = []; + throw new Error("Unknown variable reference"); } - } else if (this._evalResultProperties.has(variablesReference)) { - // the children of properties returned from an eval command are always inlined, so we simply resolve them - const property = this._evalResultProperties.get(variablesReference); - properties = property.hasChildren ? property.children : []; - } else { - throw new Error("Unknown variable reference"); - } - variables = properties.map((property) => { - const displayValue = formatPropertyValue(property); - let variablesReference: number; - let evaluateName: string; - if (property.hasChildren || property.type === "array" || property.type === "object") { - variablesReference = this._variableIdCounter++; + variables = properties.map((property) => { + const displayValue = formatPropertyValue(property); + let variablesReference: number; + let evaluateName: string; + if (property.hasChildren || property.type === "array" || property.type === "object") { + variablesReference = this._variableIdCounter++; + if (property instanceof xdebug.Property) { + this._properties.set(variablesReference, property); + } else if (property instanceof xdebug.EvalResultProperty) { + this._evalResultProperties.set(variablesReference, property); + } + } else { + variablesReference = 0; + } if (property instanceof xdebug.Property) { - this._properties.set(variablesReference, property); - } else if (property instanceof xdebug.EvalResultProperty) { - this._evalResultProperties.set(variablesReference, property); + evaluateName = property.fullName; + } else { + evaluateName = property.name; } - } else { - variablesReference = 0; - } - if (property instanceof xdebug.Property) { - evaluateName = property.fullName; - } else { - evaluateName = property.name; - } - const variable: DebugProtocol.Variable = { - name: property.name, - value: displayValue, - type: property.type, - variablesReference, - evaluateName, + const variable: DebugProtocol.Variable = { + name: property.name, + value: displayValue, + type: property.type, + variablesReference, + evaluateName, + }; + return variable; + }); + response.body = { + variables, }; - return variable; - }); - response.body = { - variables, - }; - this.sendResponse(response); + this.sendResponse(response); + } catch (error) { + this.sendErrorResponse(response, error); + } } /** * Checks the status of a StatusResponse and notifies VS Code accordingly * @param {xdebug.StatusResponse} response */ - private async _checkStatus(response: xdebug.StatusResponse): Promise { + private _checkStatus(response: xdebug.StatusResponse): void { const connection = response.connection; this._statuses.set(connection, response); - if (response.status === "stopping") { - const newResponse = await connection.sendStopCommand(); - this._checkStatus(newResponse); - } else if (response.status === "stopped") { + if (response.status === "stopped") { this.sendEvent(new ThreadEvent("exited", connection.id)); connection.close(); delete this._connection; @@ -878,79 +899,103 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments ): Promise { - this.sendResponse(response); - const xdebugResponse = await this._connection.sendRunCommand(); - this._checkStatus(xdebugResponse); + try { + const xdebugResponse = await this._connection.sendRunCommand(); + this.sendResponse(response); + this._checkStatus(xdebugResponse); + } catch (error) { + this.sendErrorResponse(response, error); + } } protected async nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): Promise { - const xdebugResponse = await this._connection.sendStepOverCommand(); - this.sendResponse(response); - this._checkStatus(xdebugResponse); + try { + const xdebugResponse = await this._connection.sendStepOverCommand(); + this.sendResponse(response); + this._checkStatus(xdebugResponse); + } catch (error) { + this.sendErrorResponse(response, error); + } } protected async stepInRequest( response: DebugProtocol.StepInResponse, args: DebugProtocol.StepInArguments ): Promise { - const xdebugResponse = await this._connection.sendStepIntoCommand(); - this.sendResponse(response); - this._checkStatus(xdebugResponse); + try { + const xdebugResponse = await this._connection.sendStepIntoCommand(); + this.sendResponse(response); + this._checkStatus(xdebugResponse); + } catch (error) { + this.sendErrorResponse(response, error); + } } protected async stepOutRequest( response: DebugProtocol.StepOutResponse, args: DebugProtocol.StepOutArguments ): Promise { - const xdebugResponse = await this._connection.sendStepOutCommand(); - this.sendResponse(response); - this._checkStatus(xdebugResponse); + try { + const xdebugResponse = await this._connection.sendStepOutCommand(); + this.sendResponse(response); + this._checkStatus(xdebugResponse); + } catch (error) { + this.sendErrorResponse(response, error); + } } protected async evaluateRequest( response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments ): Promise { - const { result } = await this._connection.sendEvalCommand(args.expression); - if (result) { - const displayValue = formatPropertyValue(result); - let variablesReference: number; - // if the property has children, generate a variable ID and save the property (including children) so VS Code can request them - if (result.hasChildren || result.type === "array" || result.type === "object") { - variablesReference = this._variableIdCounter++; - this._evalResultProperties.set(variablesReference, result); + try { + const { result } = await this._connection.sendEvalCommand(args.expression); + if (result) { + const displayValue = formatPropertyValue(result); + let variablesReference: number; + // if the property has children, generate a variable ID and save the property (including children) so VS Code can request them + if (result.hasChildren || result.type === "array" || result.type === "object") { + variablesReference = this._variableIdCounter++; + this._evalResultProperties.set(variablesReference, result); + } else { + variablesReference = 0; + } + response.body = { result: displayValue, variablesReference }; } else { - variablesReference = 0; + response.body = { result: "no result", variablesReference: 0 }; } - response.body = { result: displayValue, variablesReference }; - } else { - response.body = { result: "no result", variablesReference: 0 }; + this.sendResponse(response); + } catch (error) { + this.sendErrorResponse(response, error); } - this.sendResponse(response); } protected async setVariableRequest( response: DebugProtocol.SetVariableResponse, args: DebugProtocol.SetVariableArguments ): Promise { - const { value, name, variablesReference } = args; - let property = null; - if (this._contexts.has(variablesReference)) { - // VS Code is requesting the variables for a SCOPE, so we have to do a context_get - const context = this._contexts.get(variablesReference); - const properties = await context.getProperties(); - property = properties.find((el) => el.name === name); - } else if (this._properties.has(variablesReference)) { - // VS Code is requesting the subelements for a variable, so we have to do a property_get - property = this._properties.get(variablesReference); - } - property.value = value; - await this._connection.sendPropertySetCommand(property); + try { + const { value, name, variablesReference } = args; + let property = null; + if (this._contexts.has(variablesReference)) { + // VS Code is requesting the variables for a SCOPE, so we have to do a context_get + const context = this._contexts.get(variablesReference); + const properties = await context.getProperties(); + property = properties.find((el) => el.name === name); + } else if (this._properties.has(variablesReference)) { + // VS Code is requesting the subelements for a variable, so we have to do a property_get + property = this._properties.get(variablesReference); + } + property.value = value; + await this._connection.sendPropertySetCommand(property); - response.body = { - value: args.value, - }; - this.sendResponse(response); + response.body = { + value: args.value, + }; + this.sendResponse(response); + } catch (error) { + this.sendErrorResponse(response, error); + } } protected sendErrorResponse(response: DebugProtocol.Response, error: Error, dest?: ErrorDestination): void; diff --git a/src/extension.ts b/src/extension.ts index 503a6898..565e3bbd 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -228,7 +228,7 @@ const resolvedConnSpecs = new Map(); * @param uri if passed, re-check the `objectscript.conn.docker-compose` case in case servermanager API couldn't do that because we're still running our own `activate` method. */ export async function resolveConnectionSpec(serverName: string, uri?: vscode.Uri): Promise { - if (!serverManagerApi || !serverManagerApi.getServerSpec || serverName === "") { + if (!serverManagerApi || !serverManagerApi.getServerSpec || !serverName) { return; } if (resolvedConnSpecs.has(serverName)) { @@ -795,7 +795,7 @@ function sendWsFolderTelemetryEvent(wsFolders: readonly vscode.WorkspaceFolder[] scheme: wsFolder.uri.scheme, added: String(added), isWeb: serverSide ? String(csp) : undefined, - isProject: serverSide ? String(project.length) : undefined, + isProject: serverSide ? String(project.length > 0) : undefined, hasNs: serverSide ? String(typeof ns == "string") : undefined, serverVersion: api.active ? api.config.serverVersion : undefined, "config.syncLocalChanges": !serverSide ? conf.get("syncLocalChanges") : undefined, @@ -1152,10 +1152,14 @@ export async function activate(context: vscode.ExtensionContext): Promise { }), vscode.commands.registerCommand("vscode-objectscript.compileFolder", (_file, files) => { sendCommandTelemetryEvent("compileFolder"); + if (!_file && !files?.length) return; + files = files ?? [_file]; Promise.all(files.map((file) => importFileOrFolder(file, false))); }), vscode.commands.registerCommand("vscode-objectscript.importFolder", (_file, files) => { sendCommandTelemetryEvent("importFolder"); + if (!_file && !files?.length) return; + files = files ?? [_file]; Promise.all(files.map((file) => importFileOrFolder(file, true))); }), vscode.commands.registerCommand("vscode-objectscript.export", () => { @@ -1299,7 +1303,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { superclass(); }), vscode.commands.registerCommand("vscode-objectscript.serverActions", () => { - sendCommandTelemetryEvent("serverActions"); // TODO remove? + sendCommandTelemetryEvent("serverActions"); serverActions(); }), vscode.commands.registerCommand("vscode-objectscript.touchBar.viewOthers", () => { diff --git a/src/providers/DocumentLinkProvider.ts b/src/providers/DocumentLinkProvider.ts index e1b5f58f..b4e2d7b6 100644 --- a/src/providers/DocumentLinkProvider.ts +++ b/src/providers/DocumentLinkProvider.ts @@ -1,5 +1,6 @@ import * as vscode from "vscode"; import { DocumentContentProvider } from "./DocumentContentProvider"; +import { handleError } from "../utils"; interface StudioLink { uri: vscode.Uri; @@ -35,12 +36,14 @@ export class DocumentLinkProvider implements vscode.DocumentLinkProvider { offset = parseInt(match[2]); } + const uri = DocumentContentProvider.getUri(filename); + if (!uri) return; documentLinks.push({ range: new vscode.Range( new vscode.Position(i, match.index), new vscode.Position(i, match.index + match[0].length) ), - uri: DocumentContentProvider.getUri(filename), + uri, filename, methodname, offset, @@ -52,7 +55,10 @@ export class DocumentLinkProvider implements vscode.DocumentLinkProvider { } public async resolveDocumentLink(link: StudioLink, token: vscode.CancellationToken): Promise { - const editor = await vscode.window.showTextDocument(link.uri); + const editor = await vscode.window + .showTextDocument(link.uri) + .then(undefined, (error) => handleError(error, "Failed to resolve DocumentLink to a specific location.")); + if (!editor) return; let offset = link.offset; // add the offset of the method if it is a class @@ -65,9 +71,8 @@ export class DocumentLinkProvider implements vscode.DocumentLinkProvider { } const line = editor.document.lineAt(offset); - const range = new vscode.Range(line.range.start, line.range.start); - editor.selection = new vscode.Selection(range.start, range.start); - editor.revealRange(range, vscode.TextEditorRevealType.InCenter); + editor.selection = new vscode.Selection(line.range.start, line.range.start); + editor.revealRange(line.range, vscode.TextEditorRevealType.InCenter); return new vscode.DocumentLink(link.range, link.uri); } diff --git a/src/providers/FileSystemProvider/TextSearchProvider.ts b/src/providers/FileSystemProvider/TextSearchProvider.ts index 78da075e..2821ba27 100644 --- a/src/providers/FileSystemProvider/TextSearchProvider.ts +++ b/src/providers/FileSystemProvider/TextSearchProvider.ts @@ -3,7 +3,7 @@ import { makeRe } from "minimatch"; import { AsyncSearchRequest, SearchResult, SearchMatch } from "../../api/atelier"; import { AtelierAPI } from "../../api"; import { DocumentContentProvider } from "../DocumentContentProvider"; -import { handleError, notNull, outputChannel, RateLimiter } from "../../utils"; +import { handleError, notNull, outputChannel, RateLimiter, stringifyError } from "../../utils"; import { fileSpecFromURI, isfsConfig, IsfsUriParam } from "../../utils/FileProviderUtil"; /** @@ -235,8 +235,9 @@ async function processSearchResults( fileResults .filter((r) => r.status == "rejected") .forEach((r: PromiseRejectedResult) => { - outputChannel.appendLine(typeof r.reason == "object" ? r.reason.toString() : String(r.reason)); + outputChannel.appendLine(stringifyError(r)); }); + outputChannel.show(true); message = { text: `Failed to display results from ${rejected} file${ rejected > 1 ? "s" : "" diff --git a/src/utils/documentIndex.ts b/src/utils/documentIndex.ts index 737717c3..8f6aaace 100644 --- a/src/utils/documentIndex.ts +++ b/src/utils/documentIndex.ts @@ -228,7 +228,9 @@ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Pr change = await updateIndexForDocument(uri, documents, uris); } else if (sync && isImportableLocalFile(uri)) { change.addedOrChanged = await getCurrentFile(uri); - sendClientSideSyncTelemetryEvent(change.addedOrChanged.fileName.split(".").pop().toLowerCase()); + if (change.addedOrChanged?.fileName) { + sendClientSideSyncTelemetryEvent(change.addedOrChanged.fileName.split(".").pop().toLowerCase()); + } } if (!sync || (!change.addedOrChanged && !change.removed)) return; if (change.addedOrChanged) {