diff --git a/src/R/.Rbuildignore b/src/R/.Rbuildignore
new file mode 100644
index 0000000..86faf1e
--- /dev/null
+++ b/src/R/.Rbuildignore
@@ -0,0 +1,3 @@
+^R\.Rproj$
+^\.Rproj\.user$
+^LICENSE\.md$
diff --git a/src/R/.gitignore b/src/R/.gitignore
new file mode 100644
index 0000000..cd67eac
--- /dev/null
+++ b/src/R/.gitignore
@@ -0,0 +1 @@
+.Rproj.user
diff --git a/src/R/DESCRIPTION b/src/R/DESCRIPTION
new file mode 100644
index 0000000..ac23b01
--- /dev/null
+++ b/src/R/DESCRIPTION
@@ -0,0 +1,21 @@
+Package: rtestexplorer
+Title: Test Reporters For R Test Explorer
+Version: 0.0.0.9000
+Date: 2021-03-23
+Authors@R:
+ person(given = "Kirill",
+ family = "Müller",
+ role = c("aut", "cre"),
+ email = "krlmlr+r@mailbox.org",
+ comment = c(ORCID = "0000-0002-1416-3412"))
+Description: A test reporter for the R Test Explorer.
+License: MIT + file LICENSE
+Encoding: UTF-8
+LazyData: true
+Roxygen: list(markdown = TRUE)
+RoxygenNote: 7.1.1.9001
+Imports:
+ rlang,
+ testthat (>= 3.0.0),
+ withr
+Config/testthat/edition: 3
diff --git a/src/R/LICENSE b/src/R/LICENSE
new file mode 100644
index 0000000..aabf588
--- /dev/null
+++ b/src/R/LICENSE
@@ -0,0 +1,3 @@
+YEAR: 2021
+COPYRIGHT HOLDER: rtestexplorer authors
+COPYRIGHT HOLDER: Hadley Wickham; RStudio
diff --git a/src/R/LICENSE.md b/src/R/LICENSE.md
new file mode 100644
index 0000000..59f8613
--- /dev/null
+++ b/src/R/LICENSE.md
@@ -0,0 +1,23 @@
+# MIT License
+
+Copyright (c) 2021 rtestexplorer authors
+
+Copyright (c) 2013-2019 Hadley Wickham; RStudio
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/src/R/NAMESPACE b/src/R/NAMESPACE
new file mode 100644
index 0000000..a622156
--- /dev/null
+++ b/src/R/NAMESPACE
@@ -0,0 +1,4 @@
+# Generated by roxygen2: do not edit by hand
+
+export(VsCodeReporter)
+import(testthat)
diff --git a/src/R/R/expect.R b/src/R/R/expect.R
new file mode 100644
index 0000000..6418b3d
--- /dev/null
+++ b/src/R/R/expect.R
@@ -0,0 +1,20 @@
+expect_snapshot_reporter <- function(reporter, path = test_path("reporters/tests.R")) {
+ withr::local_rng_version("3.3")
+ withr::with_seed(1014, {
+ expect_snapshot_output(
+ with_reporter(
+ reporter,
+ test_one_file(path)
+ )
+ )
+ })
+}
+
+test_one_file <- function(path, env = test_env(), wrap = TRUE) {
+ reporter <- testthat::get_reporter()
+
+ reporter$start_file(path)
+ source_file(path, rlang::child_env(env), wrap = wrap)
+ reporter$end_context_if_started()
+ reporter$end_file()
+}
diff --git a/src/R/R/import.R b/src/R/R/import.R
new file mode 100644
index 0000000..3886d86
--- /dev/null
+++ b/src/R/R/import.R
@@ -0,0 +1,2 @@
+#' @import testthat
+NULL
diff --git a/src/R/R/reporter-vscode.R b/src/R/R/reporter-vscode.R
new file mode 100644
index 0000000..3a5c71c
--- /dev/null
+++ b/src/R/R/reporter-vscode.R
@@ -0,0 +1,98 @@
+#' Test reporter: VS Code format.
+#'
+#' This reporter will output results in a format understood by the
+#' [R Test Explorer](https://github.com/meakbiyik/vscode-r-test-adapter).
+#'
+#' @export
+VsCodeReporter <- R6::R6Class("VsCodeReporter",
+ inherit = Reporter,
+ private = list(
+ filename = NULL
+ ),
+ public = list(
+ suite_name = NULL,
+
+ initialize = function(suite_name, ...) {
+ super$initialize(...)
+ self$suite_name <- suite_name
+ private$filename <- NULL
+ self$capabilities$parallel_support <- TRUE
+ # FIXME: self$capabilities$parallel_updates <- TRUE
+ },
+
+ start_reporter = function() {
+ self$cat_json(list(type = "start_reporter", tests = list(self$suite_name)))
+ },
+
+ start_file = function(filename) {
+ self$cat_json(list(type = "start_file", filename = filename))
+ private$filename <- filename
+ },
+
+ start_test = function(context, test) {
+ self$cat_json(list(type = "start_test", test = test))
+ },
+
+ add_result = function(context, test, result) {
+ self$cat_json(list(
+ type = "add_result",
+ context = context,
+ test = test,
+ result = expectation_type(result),
+ message = exp_message(result),
+ location = expectation_location(result)
+ ))
+ },
+
+ end_test = function(context, test) {
+ self$cat_json(list(type = "end_test", test = test))
+ },
+
+ end_file = function() {
+ self$cat_json(list(type = "end_file", filename = private$filename))
+ private$filename <- NULL
+ },
+
+ end_reporter = function() {
+ self$cat_json(list(type = "end_reporter", tests = list(self$suite_name)))
+ },
+
+ cat_json = function(x) {
+ self$cat_line(jsonlite::toJSON(x, auto_unbox = TRUE))
+ }
+ )
+)
+
+expectation_type <- function(exp) {
+ stopifnot(is.expectation(exp))
+ gsub("^expectation_", "", class(exp)[[1]])
+}
+
+expectation_success <- function(exp) expectation_type(exp) == "success"
+expectation_failure <- function(exp) expectation_type(exp) == "failure"
+expectation_error <- function(exp) expectation_type(exp) == "error"
+expectation_skip <- function(exp) expectation_type(exp) == "skip"
+expectation_warning <- function(exp) expectation_type(exp) == "warning"
+expectation_broken <- function(exp) expectation_failure(exp) || expectation_error(exp)
+expectation_ok <- function(exp) expectation_type(exp) %in% c("success", "warning")
+
+exp_message <- function(x) {
+ if (expectation_error(x)) {
+ paste0("Error: ", x$message)
+ } else {
+ x$message
+ }
+}
+
+expectation_location <- function(x) {
+ if (is.null(x$srcref)) {
+ "???"
+ } else {
+ filename <- attr(x$srcref, "srcfile")$filename
+ if (identical(filename, "")) {
+ paste0("Line ", x$srcref[1])
+ } else {
+ paste0(basename(filename), ":", x$srcref[1], ":", x$srcref[2])
+ }
+ }
+}
diff --git a/src/R/man/VsCodeReporter.Rd b/src/R/man/VsCodeReporter.Rd
new file mode 100644
index 0000000..3ad9993
--- /dev/null
+++ b/src/R/man/VsCodeReporter.Rd
@@ -0,0 +1,142 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/reporter-vscode.R
+\name{VsCodeReporter}
+\alias{VsCodeReporter}
+\title{Test reporter: VS Code format.}
+\description{
+This reporter will output results in a format understood by the
+\href{https://github.com/meakbiyik/vscode-r-test-adapter}{R Test Explorer}.
+}
+\section{Super class}{
+\code{\link[testthat:Reporter]{testthat::Reporter}} -> \code{VsCodeReporter}
+}
+\section{Methods}{
+\subsection{Public methods}{
+\itemize{
+\item \href{#method-new}{\code{VsCodeReporter$new()}}
+\item \href{#method-start_reporter}{\code{VsCodeReporter$start_reporter()}}
+\item \href{#method-start_file}{\code{VsCodeReporter$start_file()}}
+\item \href{#method-start_test}{\code{VsCodeReporter$start_test()}}
+\item \href{#method-add_result}{\code{VsCodeReporter$add_result()}}
+\item \href{#method-end_test}{\code{VsCodeReporter$end_test()}}
+\item \href{#method-end_file}{\code{VsCodeReporter$end_file()}}
+\item \href{#method-end_reporter}{\code{VsCodeReporter$end_reporter()}}
+\item \href{#method-cat_json}{\code{VsCodeReporter$cat_json()}}
+\item \href{#method-clone}{\code{VsCodeReporter$clone()}}
+}
+}
+\if{html}{
+\out{Inherited methods
}
+\itemize{
+\item \out{}\href{../../testthat/html/Reporter.html#method-.start_context}{\code{testthat::Reporter$.start_context()}}\out{}
+\item \out{}\href{../../testthat/html/Reporter.html#method-cat_line}{\code{testthat::Reporter$cat_line()}}\out{}
+\item \out{}\href{../../testthat/html/Reporter.html#method-cat_tight}{\code{testthat::Reporter$cat_tight()}}\out{}
+\item \out{}\href{../../testthat/html/Reporter.html#method-end_context}{\code{testthat::Reporter$end_context()}}\out{}
+\item \out{}\href{../../testthat/html/Reporter.html#method-end_context_if_started}{\code{testthat::Reporter$end_context_if_started()}}\out{}
+\item \out{}\href{../../testthat/html/Reporter.html#method-is_full}{\code{testthat::Reporter$is_full()}}\out{}
+\item \out{}\href{../../testthat/html/Reporter.html#method-local_user_output}{\code{testthat::Reporter$local_user_output()}}\out{}
+\item \out{}\href{../../testthat/html/Reporter.html#method-rule}{\code{testthat::Reporter$rule()}}\out{}
+\item \out{}\href{../../testthat/html/Reporter.html#method-start_context}{\code{testthat::Reporter$start_context()}}\out{}
+\item \out{}\href{../../testthat/html/Reporter.html#method-update}{\code{testthat::Reporter$update()}}\out{}
+}
+\out{