Before I start. I marked some sections with #NUMBER to give you a clue that there is more info about it.
Description:
I have setup a typescript project(#1) and a build command that executes this npx tsc --noEmit --skipLibCheck --watch. (#2)
When I run that build:
I will get the error.
So far so good.
But If I correct the error
NOTE: 3:17:23 PM - Found 0 errors. Watching for file changes.
tsc reported 0 errors, so I expect to see no errors in the view, but I still do.
That is because we still have index.ts(1,5): error TS2322: Type '21' is not assignable to type 'string'. in the build output panel.
The npx tsc --noEmit --skipLibCheck --watch behaves differently when executed via the sublime build, than when executed from the terminal.
When executed via the terminal the previous tsc output is cleared before the new results are piped in.
When executed via sublime build output panel, the output is not cleared before piping in new results.
NOTE: Change the selector to match your typescript syntax selector, usually it is source.ts. (I use JSCustom, that is the reason why the selector is source.js.typescript)
#2
npx tsc - will execute the locally(in project node_modules) installed typescript compiler.
–noEmit - will emit no files(will not compile/create new files, it will just do a type check, because that is just what I want).
–skipLibCheck - will only check my code, tsc will finish faster with this flag.
– watch - will continue to run the process and watch on file changes to report back the new errors/warnings. (without me needing to execute the build again).
The reason why it doesn’t clear the output is because the exec command appends the result of stdout to the previous results, whereas I guess there is some magic going on in the typescript compiler to clear the previous results in the command prompt so you see the results of only the latest compilation, whereas with the Sublime’s build system, you see the results of every TypeScript compilation appended to the previous one and put in the output_panel of exec.
One solution I could think of is to write a custom patch for the exec command and override the method where the previous outputs are appended such that only the results of the latest compilation are appended.
import re
import sublime
import sublime_plugin
from Default.exec import ExecCommand
class CustomExecPatchCommand(ExecCommand, sublime_plugin.WindowCommand):
def run(self, **kwargs):
super().run(**kwargs)
def service_text_queue(self):
is_empty = False
with self.text_queue_lock:
if len(self.text_queue) == 0:
# this can happen if a new build was started, which will clear
# the text_queue
return
characters = self.text_queue.popleft()
is_empty = (len(self.text_queue) == 0)
# Before the new output is appended, clear anything appended already.
self.output_view.run_command("select_all")
no_of_errors = len(re.findall(r"\berror\b", characters))
for i in range(0, no_of_errors):
self.output_view.run_command("move", {
"by": "lines",
"forward": False,
"extend": True
})
self.output_view.run_command("left_delete")
# Here's where the new stdout results gets appended.
self.output_view.run_command(
'append',
{'characters': characters, 'force': True, 'scroll_to_end': True})
if self.show_errors_inline and characters.find('\n') >= 0:
errs = self.output_view.find_all_results_with_text()
errs_by_file = {}
for file, line, column, text in errs:
if file not in errs_by_file:
errs_by_file[file] = []
errs_by_file[file].append((line, column, text))
self.errs_by_file = errs_by_file
self.update_phantoms()
if not is_empty:
sublime.set_timeout(self.service_text_queue, 1)
With this in place, you should now be able to define the custom target of custom_exec_patch for your TypeScript build system.
Thanks for the detailed response
and thanks for taking the time to look at it
I will give it a go.
I guess there is some magic going on in the typescript compiler to clear the previous results in the command prompt so you see the results of only the latest compilation
If you see the images in the sublime build panel above, You will see a strange character <0x1b>. That character probably tells the terminal to clear the prompt before piping in the new text.(I guess)
For what it’s worth, you can also use the Terminus package as a custom target in a build; as a terminal emulator it should behave the same as a standard terminal/command prompt would.
Here are few things I noticed. The output panel is cleared when new text comes in, which is great.
But the next_result and prev_result commands are not working like they do with a build panel, which I use a lot. Pressing them don’t take me to the location where the error is.
And there is no inline error phantom which is nice to have.
Mostly …, well … everything is copy/pasted from the docs, and the Default.exec file
import sublime
import sublime_plugin
import subprocess
import threading
import os
import html
class MyExampleBuildCommand(sublime_plugin.WindowCommand):
encoding = 'utf-8'
killed = False
proc = None
panel = None
panel_lock = threading.Lock()
errs_by_file = {}
def is_enabled(self, kill=False):
# The Cancel build option should only be available
# when the process is still running
if kill:
return self.proc is not None and self.proc.poll() is None
return True
def run(self, cmd=[], file_regex="", line_regex="", kill=False):
if kill:
if self.proc:
self.killed = True
self.proc.terminate()
return
vars = self.window.extract_variables()
working_dir = vars['file_path']
# A lock is used to ensure only one thread is
# touching the output panel at a time
with self.panel_lock:
# Creating the panel implicitly clears any previous contents
self.panel = self.window.create_output_panel('exec')
# Enable result navigation. The result_file_regex does
# the primary matching, but result_line_regex is used
# when build output includes some entries that only
# contain line/column info beneath a previous line
# listing the file info. The result_base_dir sets the
# path to resolve relative file names against.
settings = self.panel.settings()
settings.set(
'result_file_regex',
r'{}'.format(file_regex)
)
settings.set(
'result_line_regex',
r'{}'.format(line_regex)
)
settings.set('result_base_dir', working_dir)
settings.set("scroll_past_end", False)
self.window.run_command('show_panel', {'panel': 'output.exec'})
if self.proc is not None:
self.proc.terminate()
self.proc = None
self.proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
cwd=working_dir
)
self.killed = False
threading.Thread(
target=self.read_handle,
args=(self.proc.stdout,)
).start()
def read_handle(self, handle):
chunk_size = 2 ** 13
out = b''
while True:
try:
data = os.read(handle.fileno(), chunk_size)
# If exactly the requested number of bytes was
# read, there may be more data, and the current
# data may contain part of a multibyte char
out += data
if len(data) == chunk_size:
continue
if data == b'' and out == b'':
raise IOError('EOF')
# We pass out to a function to ensure the
# timeout gets the value of out right now,
# rather than a future (mutated) version
self.queue_write(out.decode(self.encoding))
if data == b'':
raise IOError('EOF')
out = b''
except (UnicodeDecodeError) as e:
msg = 'Error decoding output using %s - %s'
self.queue_write(msg % (self.encoding, str(e)))
break
except (IOError):
if self.killed:
msg = 'Cancelled'
else:
msg = 'Finished'
self.queue_write('\n[%s]' % msg)
break
def queue_write(self, text):
sublime.set_timeout(lambda: self.do_write(text), 1)
def do_write(self, text):
with self.panel_lock:
if 'e' in text:
text = text.replace('e', '')
self.panel.run_command('clear_view_content')
self.panel.run_command('append', {'characters': text})
# Updating annotations is expensive, so batch it to the main thread
def annotations_check():
errs = self.panel.find_all_results_with_text()
if not len(errs):
self.hide_annotations()
return
errs_by_file = {}
for file, line, column, text in errs:
if file not in errs_by_file:
errs_by_file[file] = []
errs_by_file[file].append((line, column, text))
self.errs_by_file = errs_by_file
self.update_annotations()
# if text.find('\n') >= 0:
sublime.set_timeout(lambda: annotations_check())
def update_annotations(self):
stylesheet = '''
<style>
#annotation-error {
background-color: color(var(--background) blend(#fff 95%));
}
html.dark #annotation-error {
background-color: color(var(--background) blend(#fff 95%));
}
html.light #annotation-error {
background-color: color(var(--background) blend(#000 85%));
}
a {
text-decoration: inherit;
}
</style>
'''
for file, errs in self.errs_by_file.items():
view = self.window.find_open_file(file)
if view:
selection_set = []
content_set = []
line_err_set = []
for line, column, text in errs:
pt = view.text_point(line - 1, column - 1)
if (line_err_set and
line == line_err_set[len(line_err_set) - 1][0]):
line_err_set[len(line_err_set) - 1][1] += (
"<br>" + html.escape(text, quote=False))
else:
pt_b = pt + 1
if view.classify(pt) & sublime.CLASS_WORD_START:
pt_b = view.find_by_class(
pt,
forward=True,
classes=(sublime.CLASS_WORD_END))
if pt_b <= pt:
pt_b = pt + 1
selection_set.append(
sublime.Region(pt, pt_b))
line_err_set.append(
[line, html.escape(text, quote=False)])
for text in line_err_set:
content_set.append(
'<body>' + stylesheet +
'<div class="error" id=annotation-error>' +
'<span class="content">' + text[1] + '</span></div>' +
'</body>')
view.add_regions(
"exec",
selection_set,
scope="invalid",
annotations=content_set,
flags=(sublime.DRAW_SQUIGGLY_UNDERLINE |
sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE),
on_close=self.hide_annotations)
def hide_annotations(self):
for window in sublime.windows():
for file, errs in self.errs_by_file.items():
view = window.find_open_file(file)
if view:
view.erase_regions("exec")
view.hide_popup()
view = sublime.active_window().active_view()
if view:
view.erase_regions("exec")
view.hide_popup()
self.errs_by_file = {}
self.annotation_sets_by_buffer = {}
self.show_errors_inline = False
class ClearViewContent(sublime_plugin.TextCommand):
def run(self, edit):
self.view.erase(edit, sublime.Region(0, self.view.size()))