Skip to content

Commit

Permalink
Properly re-render problem widget and fix problem matching (#12802)
Browse files Browse the repository at this point in the history
- Ensure we re-render the tree model if the problem widget is activated
- Update Ansi stripping to catch Windows 'clear screen' and other codes
- Consistently use Theia URI in problem matcher protocol

- Report exit status to terminal widget to avoid resize warning

#12724
  • Loading branch information
martin-fleck-at authored Aug 24, 2023
1 parent 0184f16 commit 1f94559
Show file tree
Hide file tree
Showing 10 changed files with 136 additions and 52 deletions.
7 changes: 6 additions & 1 deletion packages/markers/src/browser/problem/problem-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { ProblemTreeModel } from './problem-tree-model';
import { MarkerInfoNode, MarkerNode, MarkerRootNode } from '../marker-tree';
import {
TreeWidget, TreeProps, ContextMenuRenderer, TreeNode, NodeProps, TreeModel,
ApplicationShell, Navigatable, ExpandableTreeNode, SelectableTreeNode, TREE_NODE_INFO_CLASS, codicon
ApplicationShell, Navigatable, ExpandableTreeNode, SelectableTreeNode, TREE_NODE_INFO_CLASS, codicon, Message
} from '@theia/core/lib/browser';
import { DiagnosticSeverity } from '@theia/core/shared/vscode-languageserver-protocol';
import * as React from '@theia/core/shared/react';
Expand Down Expand Up @@ -73,6 +73,11 @@ export class ProblemWidget extends TreeWidget {
}));
}

protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.update();
}

protected updateFollowActiveEditor(): void {
this.toDisposeOnCurrentWidgetChanged.dispose();
this.toDispose.push(this.toDisposeOnCurrentWidgetChanged);
Expand Down
68 changes: 66 additions & 2 deletions packages/process/src/node/terminal-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
// *****************************************************************************

import { injectable, inject, named } from '@theia/core/shared/inversify';
import { isWindows } from '@theia/core';
import { Disposable, DisposableCollection, Emitter, Event, isWindows } from '@theia/core';
import { ILogger } from '@theia/core/lib/common';
import { Process, ProcessType, ProcessOptions, /* ProcessErrorEvent */ } from './process';
import { ProcessManager } from './process-manager';
Expand Down Expand Up @@ -54,6 +54,8 @@ export enum NodePtyErrors {
export class TerminalProcess extends Process {

protected readonly terminal: IPty | undefined;
private _delayedResizer: DelayedResizer | undefined;
private _exitCode: number | undefined;

readonly outputStream = this.createOutputStream();
readonly errorStream = new DevNullStream({ autoDestroy: true });
Expand All @@ -79,6 +81,19 @@ export class TerminalProcess extends Process {
}
this.logger.debug('Starting terminal process', JSON.stringify(options, undefined, 2));

// Delay resizes to avoid conpty not respecting very early resize calls
// see https://github.com/microsoft/vscode/blob/a1c783c/src/vs/platform/terminal/node/terminalProcess.ts#L177
if (isWindows) {
this._delayedResizer = new DelayedResizer();
this._delayedResizer.onTrigger(dimensions => {
this._delayedResizer?.dispose();
this._delayedResizer = undefined;
if (dimensions.cols && dimensions.rows) {
this.resize(dimensions.cols, dimensions.rows);
}
});
}

const startTerminal = (command: string): { terminal: IPty | undefined, inputStream: Writable } => {
try {
return this.createPseudoTerminal(command, options, ringBuffer);
Expand Down Expand Up @@ -148,6 +163,7 @@ export class TerminalProcess extends Process {
// signal value). If it was terminated because of a signal, the
// signal parameter will hold the signal number and code should
// be ignored.
this._exitCode = exitCode;
if (signal === undefined || signal === 0) {
this.onTerminalExit(exitCode, undefined);
} else {
Expand Down Expand Up @@ -208,8 +224,32 @@ export class TerminalProcess extends Process {
}

resize(cols: number, rows: number): void {
if (typeof cols !== 'number' || typeof rows !== 'number' || isNaN(cols) || isNaN(rows)) {
return;
}
this.checkTerminal();
this.terminal!.resize(cols, rows);
try {
// Ensure that cols and rows are always >= 1, this prevents a native exception in winpty.
cols = Math.max(cols, 1);
rows = Math.max(rows, 1);

// Delay resize if needed
if (this._delayedResizer) {
this._delayedResizer.cols = cols;
this._delayedResizer.rows = rows;
return;
}

this.terminal!.resize(cols, rows);
} catch (error) {
// swallow error if the pty has already exited
// see also https://github.com/microsoft/vscode/blob/a1c783c/src/vs/platform/terminal/node/terminalProcess.ts#L549
if (this._exitCode !== undefined &&
error.message !== 'ioctl(2) failed, EBADF' &&
error.message !== 'Cannot resize a pty that has already exited') {
throw error;
}
}
}

write(data: string): void {
Expand All @@ -224,3 +264,27 @@ export class TerminalProcess extends Process {
}

}

/**
* Tracks the latest resize event to be trigger at a later point.
*/
class DelayedResizer extends DisposableCollection {
rows: number | undefined;
cols: number | undefined;
private _timeout: NodeJS.Timeout;

private readonly _onTrigger = new Emitter<{ rows?: number; cols?: number }>();
get onTrigger(): Event<{ rows?: number; cols?: number }> { return this._onTrigger.event; }

constructor() {
super();
this.push(this._onTrigger);
this._timeout = setTimeout(() => this._onTrigger.fire({ rows: this.rows, cols: this.cols }), 1000);
this.push(Disposable.create(() => clearTimeout(this._timeout)));
}

override dispose(): void {
super.dispose();
clearTimeout(this._timeout);
}
}
2 changes: 1 addition & 1 deletion packages/task/src/browser/task-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ export class TaskService implements TaskConfigurationClient {
}
}
}
const uri = new URI(problem.resource.path).withScheme(problem.resource.scheme);
const uri = problem.resource.withScheme(problem.resource.scheme);
const document = this.monacoWorkspace.getTextDocument(uri.toString());
if (problem.description.applyTo === ApplyToKind.openDocuments && !!document ||
problem.description.applyTo === ApplyToKind.closedDocuments && !document ||
Expand Down
3 changes: 1 addition & 2 deletions packages/task/src/common/problem-matcher-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { URI } from '@theia/core';
import { Severity } from '@theia/core/lib/common/severity';
import { Diagnostic } from '@theia/core/shared/vscode-languageserver-protocol';
// TODO use URI from `@theia/core` instead
import { URI } from '@theia/core/shared/vscode-uri';

export enum ApplyToKind {
allDocuments,
Expand Down
17 changes: 7 additions & 10 deletions packages/task/src/node/process/process-task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,18 @@ import { TaskManager } from '../task-manager';
import { ProcessType, ProcessTaskInfo } from '../../common/process/task-protocol';
import { TaskExitedEvent } from '../../common/task-protocol';

// copied from https://github.com/Microsoft/vscode/blob/1.33.1/src/vs/base/common/strings.ts
// Escape codes
// http://en.wikipedia.org/wiki/ANSI_escape_code
const EL = /\x1B\x5B[12]?K/g; // Erase in line
const COLOR_START = /\x1b\[\d+(;\d+)*m/g; // Color
const COLOR_END = /\x1b\[0?m/g; // Color
// copied from https://github.com/microsoft/vscode/blob/1.79.0/src/vs/base/common/strings.ts#L736
const CSI_SEQUENCE = /(:?\x1b\[|\x9B)[=?>!]?[\d;:]*["$#'* ]?[a-zA-Z@^`{}|~]/g;

// Plus additional markers for custom `\x1b]...\x07` instructions.
const CSI_CUSTOM_SEQUENCE = /\x1b\].*?\x07/g;

export function removeAnsiEscapeCodes(str: string): string {
if (str) {
str = str.replace(EL, '');
str = str.replace(COLOR_START, '');
str = str.replace(COLOR_END, '');
str = str.replace(CSI_SEQUENCE, '').replace(CSI_CUSTOM_SEQUENCE, '');
}

return str.trimRight();
return str.trimEnd();
}

export const TaskProcessOptions = Symbol('TaskProcessOptions');
Expand Down
13 changes: 4 additions & 9 deletions packages/task/src/node/task-abstract-line-matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,9 @@ import {
ProblemMatch, ProblemMatchData, ProblemLocationKind
} from '../common/problem-matcher-protocol';
import URI from '@theia/core/lib/common/uri';
// TODO use only URI from '@theia/core'
import { URI as vscodeURI } from '@theia/core/shared/vscode-uri';
import { Severity } from '@theia/core/lib/common/severity';
import { MAX_SAFE_INTEGER } from '@theia/core/lib/common/numbers';
import { join } from 'path';

const endOfLine: string = EOL;

Expand Down Expand Up @@ -247,7 +246,7 @@ export abstract class AbstractLineMatcher {
return Severity.toDiagnosticSeverity(result);
}

private getResource(filename: string, matcher: ProblemMatcher): vscodeURI {
private getResource(filename: string, matcher: ProblemMatcher): URI {
const kind = matcher.fileLocation;
let fullPath: string | undefined;
if (kind === FileLocationKind.Absolute) {
Expand All @@ -257,19 +256,15 @@ export abstract class AbstractLineMatcher {
if (relativeFileName.startsWith('./')) {
relativeFileName = relativeFileName.slice(2);
}
fullPath = new URI(matcher.filePrefix).resolve(relativeFileName).path.toString();
fullPath = join(matcher.filePrefix, relativeFileName);
}
if (fullPath === undefined) {
throw new Error('FileLocationKind is not actionable. Does the matcher have a filePrefix? This should never happen.');
}
fullPath = fullPath.replace(/\\/g, '/');
if (fullPath[0] !== '/') {
fullPath = '/' + fullPath;
}
if (matcher.uriProvider !== undefined) {
return matcher.uriProvider(fullPath);
} else {
return vscodeURI.file(fullPath);
return URI.fromFilePath(fullPath);
}
}

Expand Down
28 changes: 14 additions & 14 deletions packages/task/src/node/task-problem-collector.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import { expect } from 'chai';
import { Severity } from '@theia/core/lib/common/severity';
import { DiagnosticSeverity } from '@theia/core/shared/vscode-languageserver-protocol';
import { ProblemCollector } from './task-problem-collector';
import { expect } from 'chai';
import { ApplyToKind, FileLocationKind, ProblemLocationKind, ProblemMatch, ProblemMatchData, ProblemMatcher } from '../common/problem-matcher-protocol';
import { Severity } from '@theia/core/lib/common/severity';
import { ProblemCollector } from './task-problem-collector';

const startStopMatcher1: ProblemMatcher = {
owner: 'test1',
Expand Down Expand Up @@ -130,23 +130,23 @@ describe('ProblemCollector', () => {

expect(allMatches.length).to.eq(3);

expect((allMatches[0] as ProblemMatchData).resource!.path).eq('/home/test/hello.go');
expect((allMatches[0] as ProblemMatchData).resource!.path.toString()).eq('/home/test/hello.go');
expect((allMatches[0] as ProblemMatchData).marker).deep.equal({
range: { start: { line: 8, character: 1 }, end: { line: 8, character: 1 } },
severity: DiagnosticSeverity.Error,
source: 'test1',
message: 'undefined: fmt.Pntln'
});

expect((allMatches[1] as ProblemMatchData).resource!.path).eq('/home/test/hello.go');
expect((allMatches[1] as ProblemMatchData).resource!.path.toString()).eq('/home/test/hello.go');
expect((allMatches[1] as ProblemMatchData).marker).deep.equal({
range: { start: { line: 9, character: 5 }, end: { line: 9, character: 5 } },
severity: DiagnosticSeverity.Error,
source: 'test1',
message: 'undefined: numb'
});

expect((allMatches[2] as ProblemMatchData).resource!.path).eq('/home/test/hello.go');
expect((allMatches[2] as ProblemMatchData).resource!.path.toString()).eq('/home/test/hello.go');
expect((allMatches[2] as ProblemMatchData).marker).deep.equal({
range: { start: { line: 14, character: 8 }, end: { line: 14, character: 8 } },
severity: DiagnosticSeverity.Error,
Expand Down Expand Up @@ -176,7 +176,7 @@ describe('ProblemCollector', () => {

expect(allMatches.length).to.eq(4);

expect((allMatches[0] as ProblemMatchData).resource!.path).eq('/home/test/test-dir.js');
expect((allMatches[0] as ProblemMatchData).resource!.path.toString()).eq('/home/test/test-dir.js');
expect((allMatches[0] as ProblemMatchData).marker).deep.equal({
range: { start: { line: 13, character: 20 }, end: { line: 13, character: 20 } },
severity: DiagnosticSeverity.Warning,
Expand All @@ -185,7 +185,7 @@ describe('ProblemCollector', () => {
code: 'semi'
});

expect((allMatches[1] as ProblemMatchData).resource!.path).eq('/home/test/test-dir.js');
expect((allMatches[1] as ProblemMatchData).resource!.path.toString()).eq('/home/test/test-dir.js');
expect((allMatches[1] as ProblemMatchData).marker).deep.equal({
range: { start: { line: 14, character: 22 }, end: { line: 14, character: 22 } },
severity: DiagnosticSeverity.Warning,
Expand All @@ -194,15 +194,15 @@ describe('ProblemCollector', () => {
code: 'semi'
});

expect((allMatches[2] as ProblemMatchData).resource!.path).eq('/home/test/test-dir.js');
expect((allMatches[2] as ProblemMatchData).resource!.path.toString()).eq('/home/test/test-dir.js');
expect((allMatches[2] as ProblemMatchData).marker).deep.equal({
range: { start: { line: 102, character: 8 }, end: { line: 102, character: 8 } },
severity: DiagnosticSeverity.Error,
source: 'test2',
message: 'Parsing error: Unexpected token inte'
});

expect((allMatches[3] as ProblemMatchData).resource!.path).eq('/home/test/more-test.js');
expect((allMatches[3] as ProblemMatchData).resource!.path.toString()).eq('/home/test/more-test.js');
expect((allMatches[3] as ProblemMatchData).marker).deep.equal({
range: { start: { line: 12, character: 8 }, end: { line: 12, character: 8 } },
severity: DiagnosticSeverity.Error,
Expand Down Expand Up @@ -232,7 +232,7 @@ describe('ProblemCollector', () => {

expect(allMatches.length).to.eq(4);

expect((allMatches[0] as ProblemMatchData).resource?.path).eq('/home/test/test-dir.js');
expect((allMatches[0] as ProblemMatchData).resource?.path.toString()).eq('/home/test/test-dir.js');
expect((allMatches[0] as ProblemMatchData).marker).deep.equal({
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
severity: DiagnosticSeverity.Warning,
Expand All @@ -241,7 +241,7 @@ describe('ProblemCollector', () => {
code: 'semi'
});

expect((allMatches[1] as ProblemMatchData).resource?.path).eq('/home/test/test-dir.js');
expect((allMatches[1] as ProblemMatchData).resource?.path.toString()).eq('/home/test/test-dir.js');
expect((allMatches[1] as ProblemMatchData).marker).deep.equal({
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
severity: DiagnosticSeverity.Warning,
Expand All @@ -250,15 +250,15 @@ describe('ProblemCollector', () => {
code: 'semi'
});

expect((allMatches[2] as ProblemMatchData).resource?.path).eq('/home/test/test-dir.js');
expect((allMatches[2] as ProblemMatchData).resource?.path.toString()).eq('/home/test/test-dir.js');
expect((allMatches[2] as ProblemMatchData).marker).deep.equal({
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
severity: DiagnosticSeverity.Error,
source: 'test2',
message: 'Parsing error: Unexpected token inte'
});

expect((allMatches[3] as ProblemMatchData).resource?.path).eq('/home/test/more-test.js');
expect((allMatches[3] as ProblemMatchData).resource?.path.toString()).eq('/home/test/more-test.js');
expect((allMatches[3] as ProblemMatchData).marker).deep.equal({
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
severity: DiagnosticSeverity.Error,
Expand Down
18 changes: 13 additions & 5 deletions packages/terminal/src/browser/terminal-widget-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,21 +206,25 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget
});
this.toDispose.push(titleChangeListenerDispose);

this.toDispose.push(this.terminalWatcher.onTerminalError(({ terminalId, error }) => {
this.toDispose.push(this.terminalWatcher.onTerminalError(({ terminalId, error, attached }) => {
if (terminalId === this.terminalId) {
this.exitStatus = { code: undefined, reason: TerminalExitReason.Process };
this.dispose();
this.logger.error(`The terminal process terminated. Cause: ${error}`);
if (!attached) {
this.dispose();
}
}
}));
this.toDispose.push(this.terminalWatcher.onTerminalExit(({ terminalId, code, reason }) => {
this.toDispose.push(this.terminalWatcher.onTerminalExit(({ terminalId, code, reason, attached }) => {
if (terminalId === this.terminalId) {
if (reason) {
this.exitStatus = { code, reason };
} else {
this.exitStatus = { code, reason: TerminalExitReason.Process };
}
this.dispose();
if (!attached) {
this.dispose();
}
}
}));
this.toDispose.push(this.toDisposeOnConnect);
Expand Down Expand Up @@ -502,6 +506,8 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget
protected async attachTerminal(id: number): Promise<number> {
const terminalId = await this.shellTerminalServer.attach(id);
if (IBaseTerminalServer.validateId(terminalId)) {
// reset exit status if a new terminal process is attached
this.exitStatus = undefined;
return terminalId;
}
this.logger.warn(`Failed attaching to terminal id ${id}, the terminal is most likely gone. Starting up a new terminal instead.`);
Expand Down Expand Up @@ -776,7 +782,9 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget
return;
}
if (!IBaseTerminalServer.validateId(this.terminalId)
|| !this.terminalService.getById(this.id)) {
|| this.exitStatus
|| !this.terminalService.getById(this.id)
) {
return;
}
const { cols, rows } = this.term;
Expand Down
Loading

0 comments on commit 1f94559

Please sign in to comment.