diff --git a/NEWS.md b/NEWS.md index 202165cd4b5..9d655b9ff27 100644 --- a/NEWS.md +++ b/NEWS.md @@ -23,18 +23,17 @@ * Multiple source panes can be opened in the main window via Global Options. (#2854) * Keyboard shortcut `F6` added to navigate focus to the next pane. (#7408) +### Configurable Paths + +* The user data folder `~/.rstudio` has been moved to `~/.local/share/rstudio`, and its location can now be customized with `XDG_DATA_HOME`. (#1846) +* `XDG_CONFIG_DIRS` can be used to specify alternate directories for server configuration files. (Pro #1607) +* It is now possible to specify the exact folder for user data and configuration files using new environment variables `RSTUDIO_DATA_HOME`, `RSTUDIO_CONFIG_DIR`, etc. (#7792) + ### Miscellaneous * The Files pane now sorts file names naturally, so that e.g. `step10.R` comes after `step9.R`. (#5766) * Added command to File pane's "More" menu to copy path to clipboard (#6344) * Table summaries are shown for `tibble` objects in R Notebooks. (#5970) -* The user data folder `~/.rstudio` has been moved to `~/.local/share/rstudio`, and its location can now be customized with `XDG_DATA_HOME`. (#1846) -* The font used in the editor and console can now be customized on RStudio Server. (#2534) -* `XDG_CONFIG_DIRS` can be used to specify alternate directories for server configuration files. (Pro #1607) -* The new option `www-same-site` provides support for the `SameSite` attribute on cookies issued by RStudio. (#6608) -* New `X-RStudio-Request` header for specifying originating URL behind path-rewriting proxies (Pro #1579) -* New `X-RStudio-Root-Path` header or the new `www-root-path` for specifying the exact path prefixes added by a path-rewriting proxy (Pro #1410). -* The option `www-url-path-prefix` was deprecated and removed. Use `www-root-path` instead. * RStudio now infers document type from shebang (e.g. #!/usr/bin/env sh) for R, Python and shell scripts (#5643) * New option to configure soft wrapping for R Markdown files, and command to change the soft wrap mode of the editor on the fly (#2341) * New Command Palette for searching and running build-in commands and add-ins (#5168) @@ -43,14 +42,22 @@ * Moved console options to a new pane in Global Options (#7047) * The Data Viewer now uses the `format()` methods defined for columns entries when available (#7239) * Add support for navigating source history with mouse forward/back buttons (#7272) -* Improved error logging of mistyped usernames when using PAM authentication (#7501) * Add ability to go directly to various Global Option panes via Command Palette (#7678) -* R6Class method defintions are now indexed and accessible by the fuzzy finder (Ctrl + .) +* R6Class method definitions are now indexed and accessible by the fuzzy finder (Ctrl + .) * The 'Preview' command for R documentation files now passes along RdMacros declared from the package DESCRIPTION file. (#6871) * Some panes didn't have commands for making them visible, now they do (#5775) * Show correct symbol for Return key in Mac menus (#6524) * Added command and button for clearing Build pane output (#6636) +### RStudio Server + +* The font used in the editor and console can now be customized on RStudio Server. (#2534) +* The new option `www-same-site` provides support for the `SameSite` attribute on cookies issued by RStudio. (#6608) +* New `X-RStudio-Request` header for specifying originating URL behind path-rewriting proxies (Pro #1579) +* New `X-RStudio-Root-Path` header or the new `www-root-path` for specifying the exact path prefixes added by a path-rewriting proxy (Pro #1410). +* The option `www-url-path-prefix` was deprecated and removed. Use `www-root-path` instead. +* Improved error logging of mistyped usernames when using PAM authentication (#7501) + ### RStudio Server Pro * SAML is now supported as an authentication mechanism (Pro #1194) diff --git a/src/cpp/core/r_util/RSessionContext.cpp b/src/cpp/core/r_util/RSessionContext.cpp index 29ba6b35d73..6964d8bb47a 100644 --- a/src/cpp/core/r_util/RSessionContext.cpp +++ b/src/cpp/core/r_util/RSessionContext.cpp @@ -152,7 +152,7 @@ std::string SessionScope::workbench() const else if (isVSCode()) return kWorkbenchVSCode; else - return kWorkbenchJupyterNotebook; + return kWorkbenchRStudio; } // This function is intended to tell us whether a given path corresponds to an diff --git a/src/cpp/core/r_util/RUserData.cpp b/src/cpp/core/r_util/RUserData.cpp index 81051a5410a..ba70635a27b 100644 --- a/src/cpp/core/r_util/RUserData.cpp +++ b/src/cpp/core/r_util/RUserData.cpp @@ -66,6 +66,12 @@ Error migrateUserStateIfNecessary(SessionType sessionType) if (oldScratchPath.completeChildPath(kMigratedFile).exists()) return Success(); + // If the new and old folders are the same, no migration is necessary (this + // could happen if RSTUDIO_DATA_HOME is used to preserve the legacy folder + // location) + if (oldScratchPath.isEquivalentTo(newPath)) + return Success(); + // Create the new folder if necessary so we can move content there. error = newPath.ensureDirectory(); if (error) diff --git a/src/cpp/core/system/Xdg.cpp b/src/cpp/core/system/Xdg.cpp index 8fe21f28b68..cdd1e4a273e 100644 --- a/src/cpp/core/system/Xdg.cpp +++ b/src/cpp/core/system/Xdg.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #endif #include @@ -25,6 +26,7 @@ #include #include #include +#include namespace rstudio { namespace core { @@ -32,7 +34,56 @@ namespace system { namespace xdg { namespace { -FilePath resolveXdgDir(const std::string& envVar, +/** + * Returns the hostname from the operating system + */ +std::string getHostname() +{ + // Use a static string to store the hostname so we don't have to look it up + // multiple times + static std::string hostname; + static boost::mutex mutex; + std::string result; + + // Lock to ensure that we don't try to read/write the hostname from two + // threads + LOCK_MUTEX(mutex) + { + if (hostname.empty()) + { + char buffer[256]; + int status = ::gethostname(buffer, 255); + if (status == 0) + { + // If successful, store the hostname for later; swallow errors here + // since they are not actionable + hostname = std::string(buffer); + } + } + result = hostname; + } + END_LOCK_MUTEX + + return result; +} + +/** + * Resolves an XDG directory based on the user and environment. + * + * @param rstudioEnvVer The RStudio-specific environment variable specifying + * the directory (given precedence) + * @param xdgEnvVar The XDG standard environment variable + * @param defaultDir Fallback default directory if neither environment variable + * is present + * @param windowsFolderId The ID of the Windows folder to resolve against + * @param user Optionally, the user to return a directory for; if omitted the + * current user is used + * @param homeDir Optionally, the home directory to resolve against; if omitted + * the current user's home directory is used + */ +FilePath resolveXdgDir( + const std::string& rstudioEnvVar, + const std::string& xdgEnvVar, #ifdef _WIN32 const GUID windowsFolderId, #endif @@ -41,7 +92,18 @@ FilePath resolveXdgDir(const std::string& envVar, const boost::optional& homeDir) { FilePath xdgHome; - std::string env = getenv(envVar); + bool finalPath = true; + + // Look for the RStudio-specific environment variable + std::string env = getenv(rstudioEnvVar); + if (env.empty()) + { + // The RStudio environment variable specifices the final path; if it isn't + // set we will need to append "rstudio" to the path later. + finalPath = false; + env = getenv(xdgEnvVar); + } + if (env.empty()) { // No root specified for xdg home; we will need to generate one. @@ -84,18 +146,36 @@ FilePath resolveXdgDir(const std::string& envVar, xdgHome = FilePath(env); } - // expand HOME and USER if given + // expand HOME, USER, and HOSTNAME if given core::system::Options environment; core::system::setenv(&environment, "HOME", homeDir ? homeDir->getAbsolutePath() : userHomePath().getAbsolutePath()); core::system::setenv(&environment, "USER", user ? *user : username()); + + // check for manually specified hostname in environment variable + std::string hostname = core::system::getenv("HOSTNAME"); + + // when omitted, look up the hostname using a system call + if (hostname.empty()) + { + hostname = getHostname(); + } + core::system::setenv(&environment, "HOSTNAME", hostname); + std::string expanded = core::system::expandEnvVars(environment, xdgHome.getAbsolutePath()); // resolve aliases in the path xdgHome = FilePath::resolveAliasedPath(expanded, homeDir ? *homeDir : userHomePath()); + // If this is the final path, we can return it as-is + if (finalPath) + { + return xdgHome; + } + + // Otherwise, it's a root folder in which we need to create our own subfolder return xdgHome.completePath( #ifdef _WIN32 "RStudio" @@ -111,7 +191,8 @@ FilePath userConfigDir( const boost::optional& user, const boost::optional& homeDir) { - return resolveXdgDir("XDG_CONFIG_HOME", + return resolveXdgDir("RSTUDIO_CONFIG_HOME", + "XDG_CONFIG_HOME", #ifdef _WIN32 FOLDERID_RoamingAppData, #endif @@ -125,7 +206,8 @@ FilePath userDataDir( const boost::optional& user, const boost::optional& homeDir) { - return resolveXdgDir("XDG_DATA_HOME", + return resolveXdgDir("RSTUDIO_DATA_HOME", + "XDG_DATA_HOME", #ifdef _WIN32 FOLDERID_LocalAppData, #endif @@ -138,24 +220,28 @@ FilePath userDataDir( FilePath systemConfigDir() { #ifndef _WIN32 - // On POSIX operating systems, it's possible to specify multiple config directories. - // We have to select one, so read the list and take the first one that contains an - // "rstudio" folder. - std::string env = getenv("XDG_CONFIG_DIRS"); - if (env.find_first_of(":") != std::string::npos) + if (getenv("RSTUDIO_CONFIG_DIR").empty()) { - std::vector dirs = algorithm::split(env, ":"); - for (const std::string& dir: dirs) + // On POSIX operating systems, it's possible to specify multiple config + // directories. We have to select one, so read the list and take the first + // one that contains an "rstudio" folder. + std::string env = getenv("XDG_CONFIG_DIRS"); + if (env.find_first_of(":") != std::string::npos) { - FilePath resolved = FilePath(dir).completePath("rstudio"); - if (resolved.exists()) + std::vector dirs = algorithm::split(env, ":"); + for (const std::string& dir: dirs) { - return resolved; + FilePath resolved = FilePath(dir).completePath("rstudio"); + if (resolved.exists()) + { + return resolved; + } } } } #endif - return resolveXdgDir("XDG_CONFIG_DIRS", + return resolveXdgDir("RSTUDIO_CONFIG_DIR", + "XDG_CONFIG_DIRS", #ifdef _WIN32 FOLDERID_ProgramData, #endif @@ -171,19 +257,22 @@ FilePath systemConfigFile(const std::string& filename) // Passthrough on Windows return systemConfigDir().completeChildPath(filename); #else - // On POSIX, check for a search path. - std::string env = getenv("XDG_CONFIG_DIRS"); - if (env.find_first_of(":") != std::string::npos) + if (getenv("RSTUDIO_CONFIG_DIR").empty()) { - // This is a search path; check each element for the file. - std::vector dirs = algorithm::split(env, ":"); - for (const std::string& dir: dirs) + // On POSIX, check for a search path. + std::string env = getenv("XDG_CONFIG_DIRS"); + if (env.find_first_of(":") != std::string::npos) { - FilePath resolved = FilePath(dir).completePath("rstudio") - .completeChildPath(filename); - if (resolved.exists()) + // This is a search path; check each element for the file. + std::vector dirs = algorithm::split(env, ":"); + for (const std::string& dir: dirs) { - return resolved; + FilePath resolved = FilePath(dir).completePath("rstudio") + .completeChildPath(filename); + if (resolved.exists()) + { + return resolved; + } } } } @@ -197,8 +286,10 @@ FilePath systemConfigFile(const std::string& filename) void forwardXdgEnvVars(Options *pEnvironment) { // forward relevant XDG environment variables (i.e. all those we respect above) - for (auto&& xdgVar: {"XDG_CONFIG_HOME", "XDG_CONFIG_DIRS", - "XDG_DATA_HOME", "XDG_DATA_DIRS"}) + for (auto&& xdgVar: {"RSTUDIO_CONFIG_HOME", "RSTUDIO_CONFIG_DIR", + "RSTUDIO_DATA_HOME", "RSTUDIO_DATA_DIR", + "XDG_CONFIG_HOME", "XDG_CONFIG_DIRS", + "XDG_DATA_HOME", "XDG_DATA_DIRS"}) { // only forward value if non-empty; avoid overwriting a previously set // value with an empty one diff --git a/src/cpp/server_core/ServerDatabase.cpp b/src/cpp/server_core/ServerDatabase.cpp index 9132d372b03..bb0ea2367f2 100644 --- a/src/cpp/server_core/ServerDatabase.cpp +++ b/src/cpp/server_core/ServerDatabase.cpp @@ -14,7 +14,7 @@ */ #include -#include +#include #include #include diff --git a/src/cpp/server_core/include/server_core/ServerDatabaseKeyObfuscation.hpp b/src/cpp/server_core/include/server_core/ServerKeyObfuscation.hpp similarity index 94% rename from src/cpp/server_core/include/server_core/ServerDatabaseKeyObfuscation.hpp rename to src/cpp/server_core/include/server_core/ServerKeyObfuscation.hpp index 83d4a1c09b6..d2970859277 100644 --- a/src/cpp/server_core/include/server_core/ServerDatabaseKeyObfuscation.hpp +++ b/src/cpp/server_core/include/server_core/ServerKeyObfuscation.hpp @@ -1,5 +1,5 @@ /* - * ServerDatabaseKeyObfuscation.hpp + * ServerKeyObfuscation.hpp * * Copyright (C) 2020 by RStudio, PBC * diff --git a/src/cpp/session/modules/rmarkdown/SessionRmdNotebook.cpp b/src/cpp/session/modules/rmarkdown/SessionRmdNotebook.cpp index 5d12355f522..5d3078b08bb 100644 --- a/src/cpp/session/modules/rmarkdown/SessionRmdNotebook.cpp +++ b/src/cpp/session/modules/rmarkdown/SessionRmdNotebook.cpp @@ -181,9 +181,18 @@ void onChunkExecCompleted(const std::string& docId, SEXP resultSEXP = R_NilValue; std::string callback; + std::string escapedLabel = + core::string_utils::jsLiteralEscape( + core::string_utils::htmlEscape(label, true)); + std::string escapedCode = + core::string_utils::jsLiteralEscape( + core::string_utils::htmlEscape(code, true)); + boost::algorithm::replace_all(escapedLabel, "-", "_"); + boost::algorithm::replace_all(escapedCode, "-", "_"); + r::exec::RFunction func(".rs.executeChunkCallback"); - func.addParam(label); - func.addParam(code); + func.addParam(escapedLabel); + func.addParam(escapedCode); core::Error error = func.call(&resultSEXP, &rProtect); if (error) diff --git a/src/gwt/panmirror/src/editor/src/api/pandoc_attr.ts b/src/gwt/panmirror/src/editor/src/api/pandoc_attr.ts index 5568d07b28d..2737bcaac28 100644 --- a/src/gwt/panmirror/src/editor/src/api/pandoc_attr.ts +++ b/src/gwt/panmirror/src/editor/src/api/pandoc_attr.ts @@ -39,7 +39,7 @@ export const kSpanChildren = 1; export interface PandocAttr { id: string; classes: string[]; - keyvalue: [[string, string]]; + keyvalue: Array<[string, string]>; } export const pandocAttrSpec = { @@ -101,7 +101,6 @@ export function pandocAttrToDomAttr(attrs: any, marker = true) { domAttr[kDataPmPandocAttr] = '1'; } - // return domAttr return domAttr; } @@ -141,6 +140,101 @@ export function pandocAttrParseDom(el: Element, attrs: { [key: string]: string | return attr; } +export function pandocAttrParseText(attr: string): PandocAttr | null { + + attr = attr.trim(); + + let id = ''; + const classes: string[] = []; + let remainder = ''; + + let current = ''; + const resolveCurrent = () => { + + const resolve = current; + current = ''; + + if (resolve.length === 0) { + return true; + } else if (resolve.startsWith('#')) { + if (id.length === 0 && resolve.length > 1) { + id = resolve.substr(1); + return true; + } else { + return false; + } + } else if (resolve.startsWith('.')) { + if (resolve.length > 1) { + classes.push(resolve.substr(1)); + return true; + } else { + return false; + } + } else { + remainder = resolve; + return true; + } + }; + + for (let i = 0; i < attr.length; i++) { + let inQuotes = false; + const ch = attr[i]; + inQuotes = ch === '"' ? !inQuotes : inQuotes; + if (ch !== ' ' && !inQuotes) { + current += ch; + } else if (resolveCurrent()) { + + // if we have a remainder then the rest of the string is the remainder + if (remainder.length > 0) { + remainder = remainder + attr.substr(i); + break; + } + + } else { + return null; + } + } + + if (resolveCurrent()) { + if (id.length === 0 && classes.length === 0) { + remainder = attr; + } + return { + id, + classes, + keyvalue: remainder.length > 0 ? pandocAttrKeyvalueFromText(remainder, ' ') : [] + }; + } else { + return null; + } + +} + +export function pandocAttrKeyvalueFromText(text: string, separator: ' ' | '\n'): Array<[string, string]> { + + // if the separator is a space then convert unquoted spaces to newline + if (separator === ' ') { + let convertedText = ''; + let inQuotes = false; + for (let i = 0; i < text.length; i++) { + let ch = text.charAt(i); + if (ch === '"') { + inQuotes = !inQuotes; + } else if (ch === ' ' && !inQuotes) { + ch = '\n'; + } + convertedText += ch; + } + text = convertedText; + } + + const lines = text.trim().split('\n'); + return lines.map(line => { + const parts = line.trim().split('='); + return [parts[0], (parts[1] || '').replace(/^"/, '').replace(/"$/, '')]; + }); +} + export interface AttrKeyvaluePartitioned { base: Array<[string, string]>; diff --git a/src/gwt/panmirror/src/editor/src/api/rmd.ts b/src/gwt/panmirror/src/editor/src/api/rmd.ts index e46264f8411..5ee1e566ff6 100644 --- a/src/gwt/panmirror/src/editor/src/api/rmd.ts +++ b/src/gwt/panmirror/src/editor/src/api/rmd.ts @@ -29,6 +29,7 @@ import { getMarkRange } from './mark'; import { precedingListItemInsertPos, precedingListItemInsert } from './list'; import { toggleBlockType } from './command'; import { selectionIsBodyTopLevel } from './selection'; +import { uuidv4 } from './util'; export interface EditorRmdChunk { lang: string; @@ -86,7 +87,7 @@ export function insertRmdChunk(chunkPlaceholder: string, rowOffset = 0, colOffse // perform insert const tr = state.tr; const rmdText = schema.text(chunkPlaceholder); - const rmdNode = schema.nodes.rmd_chunk.create({}, rmdText); + const rmdNode = schema.nodes.rmd_chunk.create({ navigation_id: uuidv4() }, rmdText); const prevListItemPos = precedingListItemInsertPos(tr.doc, tr.selection); if (prevListItemPos) { precedingListItemInsert(tr, prevListItemPos, rmdNode); diff --git a/src/gwt/panmirror/src/editor/src/api/ui-dialogs.ts b/src/gwt/panmirror/src/editor/src/api/ui-dialogs.ts index feac0d33894..fae232b9c64 100644 --- a/src/gwt/panmirror/src/editor/src/api/ui-dialogs.ts +++ b/src/gwt/panmirror/src/editor/src/api/ui-dialogs.ts @@ -19,7 +19,7 @@ import { ListCapabilities, ListType } from "./list"; import { TableCapabilities } from "./table"; import { CSL } from "./csl"; import { CiteField } from "./cite"; -import { kStyleAttrib, attrPartitionKeyvalue } from "./pandoc_attr"; +import { kStyleAttrib, attrPartitionKeyvalue, pandocAttrKeyvalueFromText } from "./pandoc_attr"; export interface EditorDialogs { alert: AlertFn; @@ -200,7 +200,7 @@ export function attrInputToProps(attr: AttrEditInput): AttrProps { if (attr.style) { text += `\nstyle=${attr.style}\n`; } - keyvalue = attrKeyvalueFromText(text); + keyvalue = pandocAttrKeyvalueFromText(text, '\n'); } return { id: asPandocId(attr.id || ''), @@ -209,13 +209,7 @@ export function attrInputToProps(attr: AttrEditInput): AttrProps { }; } -function attrKeyvalueFromText(text: string): Array<[string, string]> { - const lines = text.trim().split('\n'); - return lines.map(line => { - const parts = line.trim().split('='); - return [parts[0], (parts[1] || '').replace(/^"/, '').replace(/"$/, '')]; - }); -} + function asPandocId(id: string) { return id.replace(/^#/, ''); diff --git a/src/gwt/panmirror/src/editor/src/api/widgets/button.tsx b/src/gwt/panmirror/src/editor/src/api/widgets/button.tsx index 760e9b21c70..f7eb4e7afc3 100644 --- a/src/gwt/panmirror/src/editor/src/api/widgets/button.tsx +++ b/src/gwt/panmirror/src/editor/src/api/widgets/button.tsx @@ -54,6 +54,7 @@ export const LinkButton: React.FC = props => { export interface ImageButtonProps extends WidgetProps { title: string; image: string; + tabIndex?: number; onClick?: () => void; } @@ -66,7 +67,7 @@ export const ImageButton = React.forwardRef } }; return ( - ); diff --git a/src/gwt/panmirror/src/editor/src/behaviors/attr_edit/attr_edit-decoration.tsx b/src/gwt/panmirror/src/editor/src/behaviors/attr_edit/attr_edit-decoration.tsx index 8e8389609e8..14af47adc59 100644 --- a/src/gwt/panmirror/src/editor/src/behaviors/attr_edit/attr_edit-decoration.tsx +++ b/src/gwt/panmirror/src/editor/src/behaviors/attr_edit/attr_edit-decoration.tsx @@ -68,6 +68,7 @@ const AttrEditDecoration: React.FC = props => { classes={['attr-edit-button']} image={props.ui.prefs.darkMode() ? props.ui.images.properties_deco_dark! : props.ui.images.properties_deco!} title={buttonTitle} + tabIndex={-1} onClick={onClick} /> ) : null} diff --git a/src/gwt/panmirror/src/editor/src/nodes/heading.ts b/src/gwt/panmirror/src/editor/src/nodes/heading.ts index af108ba8e3b..13067c2f6b7 100644 --- a/src/gwt/panmirror/src/editor/src/nodes/heading.ts +++ b/src/gwt/panmirror/src/editor/src/nodes/heading.ts @@ -13,15 +13,15 @@ * */ -import { textblockTypeInputRule } from 'prosemirror-inputrules'; +import { textblockTypeInputRule, InputRule } from 'prosemirror-inputrules'; import { Node as ProsemirrorNode, Schema, NodeType, Fragment } from 'prosemirror-model'; import { EditorState } from 'prosemirror-state'; -import { findParentNode } from 'prosemirror-utils'; +import { findParentNode, findParentNodeOfType } from 'prosemirror-utils'; import { PandocOutput, PandocToken, PandocTokenType } from '../api/pandoc'; import { EditorCommandId, toggleBlockType, ProsemirrorCommand } from '../api/command'; import { Extension, ExtensionContext } from '../api/extension'; -import { pandocAttrSpec, pandocAttrParseDom, pandocAttrToDomAttr, pandocAttrReadAST } from '../api/pandoc_attr'; +import { pandocAttrSpec, pandocAttrParseDom, pandocAttrToDomAttr, pandocAttrReadAST, pandocAttrParseText } from '../api/pandoc_attr'; import { uuidv4 } from '../api/util'; import { EditorUI } from '../api/ui'; import { OmniInsert, OmniInsertGroup } from '../api/omni_insert'; @@ -133,7 +133,7 @@ const extension = (context: ExtensionContext): Extension => { }, inputRules: (schema: Schema) => { - return [ + const rules = [ textblockTypeInputRule( new RegExp('^(#{1,' + kHeadingLevels.length + '})\\s$'), schema.nodes.heading, @@ -142,7 +142,15 @@ const extension = (context: ExtensionContext): Extension => { navigation_id: uuidv4(), }), ), + ]; + + if (headingAttr) { + rules.push(headingAttributeInputRule(schema)); + } + + return rules; + }, plugins: (schema: Schema) => { @@ -151,6 +159,31 @@ const extension = (context: ExtensionContext): Extension => { }; }; +function headingAttributeInputRule(schema: Schema) { + return new InputRule(/ {([^}]+)}$/, (state: EditorState, match: string[], start: number, end: number) => { + // only fire in headings + const heading = findParentNodeOfType(schema.nodes.heading)(state.selection); + if (heading) { + // try to parse the attributes + const attrs = pandocAttrParseText(match[1]); + if (attrs) { + const tr = state.tr; + tr.setNodeMarkup(heading.pos, undefined, { + ...heading.node.attrs, + ...attrs + }); + tr.deleteRange(start + 1, end); + return tr; + } else { + return null; + } + } else { + return null; + } + + }); +} + class HeadingCommand extends ProsemirrorCommand { public readonly nodeType: NodeType; public readonly level: number; diff --git a/src/gwt/panmirror/src/editor/src/nodes/image/figure.ts b/src/gwt/panmirror/src/editor/src/nodes/image/figure.ts index f89733d5c60..31fca71716b 100644 --- a/src/gwt/panmirror/src/editor/src/nodes/image/figure.ts +++ b/src/gwt/panmirror/src/editor/src/nodes/image/figure.ts @@ -93,6 +93,7 @@ const extension = (context: ExtensionContext): Extension => { const handleHTMLImage = (html: string) => { const attrs = imageAttrsFromHTML(html); if (attrs) { + attrs.raw = true; writer.addNode(schema.nodes.figure, attrs, []); return true; } else { diff --git a/src/gwt/panmirror/src/editor/src/nodes/image/image.ts b/src/gwt/panmirror/src/editor/src/nodes/image/image.ts index c2c1aa78b6f..64d1550dd6b 100644 --- a/src/gwt/panmirror/src/editor/src/nodes/image/image.ts +++ b/src/gwt/panmirror/src/editor/src/nodes/image/image.ts @@ -168,13 +168,13 @@ export function imagePandocOutputWriter(figure: boolean, ui: EditorUI) { }; // see if we need to write raw html - const writeHTML = + const requireHTML = pandocAttrAvailable(node.attrs) && // attribs need to be written !output.extensions.link_attributes && // markdown attribs not supported output.extensions.raw_html; // raw html is supported // if we do, then substitute a raw html writer - if (writeHTML) { + if (node.attrs.raw || requireHTML) { writer = () => { const imgAttr = imageDOMAttributes(node, true, false); const html = asHTMLTag('img', imgAttr, true, true); @@ -207,6 +207,7 @@ function imageInlineHTMLReader(schema: Schema, html: string, writer?: Prosemirro if (writer) { const attrs = imageAttrsFromHTML(html); if (attrs) { + attrs.raw = true; writer.addNode(schema.nodes.image, attrs, []); } else { return false; @@ -248,6 +249,7 @@ export function imageNodeAttrsSpec(linkTo: boolean, imageAttributes: boolean) { src: {}, title: { default: null }, alt: { default: null }, + raw: { default: false }, ...(linkTo ? { linkTo: { default: null } } : {}), ...(imageAttributes ? pandocAttrSpec : {}), }; diff --git a/src/gwt/panmirror/src/editor/src/optional/ace/ace-render-queue.ts b/src/gwt/panmirror/src/editor/src/optional/ace/ace-render-queue.ts index c7145577658..c0baa5e2f84 100644 --- a/src/gwt/panmirror/src/editor/src/optional/ace/ace-render-queue.ts +++ b/src/gwt/panmirror/src/editor/src/optional/ace/ace-render-queue.ts @@ -206,16 +206,18 @@ export class AceRenderQueue { this.needsSort = false; } - // Pop the next view (editor instance) to be rendered off the stack - const view = this.renderQueue.shift(); + // Pop the next view (editor instance) to be rendered off the stack. + // Fast-forward past instances that no longer have a position; these can + // accumulate when NodeViews are added to the render queue but replaced + // (by a document rebuild) before they have a chance to render. + let view: AceNodeView | undefined; + while (view === null || view === undefined || view.getPos() === undefined) { + view = this.renderQueue.shift(); + } // Render this view if (view) { - // Don't render if the view no longer has a position (this can happen if - // the view was moved or deleted before it got a chance to render) - if (view.getPos() !== undefined) { - view.initEditor(); - } + view.initEditor(); } if (this.renderQueue.length > 0) { diff --git a/src/gwt/panmirror/src/editor/src/pandoc/pandoc_from_prosemirror.ts b/src/gwt/panmirror/src/editor/src/pandoc/pandoc_from_prosemirror.ts index c6637af098a..3f13280ba64 100644 --- a/src/gwt/panmirror/src/editor/src/pandoc/pandoc_from_prosemirror.ts +++ b/src/gwt/panmirror/src/editor/src/pandoc/pandoc_from_prosemirror.ts @@ -204,7 +204,7 @@ class PandocWriter implements PandocOutput { this.write(arr); } - public writeAttr(id?: string, classes?: string[], keyvalue?: [[string, string]]) { + public writeAttr(id?: string, classes?: string[], keyvalue?: Array<[string, string]>) { this.write([id || '', classes || [], keyvalue || []]); } diff --git a/src/gwt/src/org/rstudio/studio/client/workbench/views/source/SourceColumnManager.java b/src/gwt/src/org/rstudio/studio/client/workbench/views/source/SourceColumnManager.java index 06153d927a0..9a19b81a4d7 100644 --- a/src/gwt/src/org/rstudio/studio/client/workbench/views/source/SourceColumnManager.java +++ b/src/gwt/src/org/rstudio/studio/client/workbench/views/source/SourceColumnManager.java @@ -1343,8 +1343,14 @@ public void disownDocOnDrag(String docId, SourceColumn column) Debug.logWarning("Warning: No column was provided to remove the doc from."); column = getActive(); } + boolean setNewActiveEditor = false; + if (column == getActive() && column.getEditors().size() > 1) + setNewActiveEditor = true; + column.closeDoc(docId); column.cancelTabDrag(); + if (setNewActiveEditor) + column.setActiveEditor(); } public void selectTab(EditingTarget target) diff --git a/src/gwt/src/org/rstudio/studio/client/workbench/views/source/editors/text/ChunkOutputWidget.java b/src/gwt/src/org/rstudio/studio/client/workbench/views/source/editors/text/ChunkOutputWidget.java index b0ddfd29fdd..028bf2696c0 100644 --- a/src/gwt/src/org/rstudio/studio/client/workbench/views/source/editors/text/ChunkOutputWidget.java +++ b/src/gwt/src/org/rstudio/studio/client/workbench/views/source/editors/text/ChunkOutputWidget.java @@ -87,15 +87,6 @@ interface ChunkOutputWidgetUiBinder public interface Resources extends ClientBundle { - @Source("ExpandChunkIcon_2x.png") - ImageResource expandChunkIcon2x(); - - @Source("CollapseChunkIcon_2x.png") - ImageResource collapseChunkIcon2x(); - - @Source("RemoveChunkIcon_2x.png") - ImageResource removeChunkIcon2x(); - @Source("PopoutChunkIcon_2x.png") ImageResource popoutIcon2x(); } @@ -129,11 +120,6 @@ public ChunkOutputWidget(String documentId, String chunkId, ChunkDataWidget.injectPagedTableResources(); - clear_.addStyleName("rstudio-themes-inverts"); - clear_.addStyleName("rstudio-classic-inverts"); - expand_.addStyleName("rstudio-themes-inverts"); - expand_.addStyleName("rstudio-classic-inverts"); - if (chunkOutputSize_ == ChunkOutputSize.Default) { frame_.getElement().getStyle().setHeight( @@ -956,8 +942,8 @@ private boolean isBlockType(int type) return true; } - @UiField Image clear_; - @UiField Image expand_; + @UiField HTMLPanel clear_; + @UiField HTMLPanel expand_; @UiField Image popout_; @UiField SimplePanel root_; @UiField ChunkStyle style; diff --git a/src/gwt/src/org/rstudio/studio/client/workbench/views/source/editors/text/ChunkOutputWidget.ui.xml b/src/gwt/src/org/rstudio/studio/client/workbench/views/source/editors/text/ChunkOutputWidget.ui.xml index f341d740bf8..4170826c131 100644 --- a/src/gwt/src/org/rstudio/studio/client/workbench/views/source/editors/text/ChunkOutputWidget.ui.xml +++ b/src/gwt/src/org/rstudio/studio/client/workbench/views/source/editors/text/ChunkOutputWidget.ui.xml @@ -5,7 +5,20 @@ + + + + + + + @external editor_dark; + + @url COLLAPSE_LIGHT collapseChunkIcon2x; + @url COLLAPSE_DARK collapseChunkIconDark2x; + @url REMOVE_LIGHT removeChunkIcon2x; + @url REMOVE_DARK removeChunkIconDark2x; + .fullsize { padding: 0; @@ -25,6 +38,33 @@ z-index: 25; } + .clearIcon, .expandIcon + { + background-size: 100%; + width: 11px; + height: 10px; + } + + .clearIcon + { + background-image: REMOVE_LIGHT; + } + + .editor_dark .clearIcon + { + background-image: REMOVE_DARK; + } + + .expandIcon + { + background-image: COLLAPSE_LIGHT; + } + + .editor_dark .expandIcon + { + background-image: COLLAPSE_DARK; + } + .expand { width: 11px !important; @@ -208,6 +248,7 @@ opacity: 0.5; transition: opacity 400ms ease; } + - - + + + + ui:field="expand_"> + + diff --git a/src/gwt/src/org/rstudio/studio/client/workbench/views/source/editors/text/CollapseChunkIconDark_2x.png b/src/gwt/src/org/rstudio/studio/client/workbench/views/source/editors/text/CollapseChunkIconDark_2x.png new file mode 100644 index 00000000000..6122e9f89b0 Binary files /dev/null and b/src/gwt/src/org/rstudio/studio/client/workbench/views/source/editors/text/CollapseChunkIconDark_2x.png differ diff --git a/src/gwt/src/org/rstudio/studio/client/workbench/views/source/editors/text/ExpandChunkIcon.png b/src/gwt/src/org/rstudio/studio/client/workbench/views/source/editors/text/ExpandChunkIcon.png deleted file mode 100644 index 6d34fc9e9f4..00000000000 Binary files a/src/gwt/src/org/rstudio/studio/client/workbench/views/source/editors/text/ExpandChunkIcon.png and /dev/null differ diff --git a/src/gwt/src/org/rstudio/studio/client/workbench/views/source/editors/text/ExpandChunkIcon_2x.png b/src/gwt/src/org/rstudio/studio/client/workbench/views/source/editors/text/ExpandChunkIcon_2x.png deleted file mode 100644 index ca7b690af8b..00000000000 Binary files a/src/gwt/src/org/rstudio/studio/client/workbench/views/source/editors/text/ExpandChunkIcon_2x.png and /dev/null differ diff --git a/src/gwt/src/org/rstudio/studio/client/workbench/views/source/editors/text/RemoveChunkIconDark_2x.png b/src/gwt/src/org/rstudio/studio/client/workbench/views/source/editors/text/RemoveChunkIconDark_2x.png new file mode 100644 index 00000000000..716c29729ac Binary files /dev/null and b/src/gwt/src/org/rstudio/studio/client/workbench/views/source/editors/text/RemoveChunkIconDark_2x.png differ diff --git a/src/gwt/src/org/rstudio/studio/client/workbench/views/source/editors/text/visualmode/VisualMode.java b/src/gwt/src/org/rstudio/studio/client/workbench/views/source/editors/text/visualmode/VisualMode.java index 4719014b4b7..81eed73f493 100644 --- a/src/gwt/src/org/rstudio/studio/client/workbench/views/source/editors/text/visualmode/VisualMode.java +++ b/src/gwt/src/org/rstudio/studio/client/workbench/views/source/editors/text/visualmode/VisualMode.java @@ -1336,10 +1336,7 @@ private PanmirrorOptions panmirrorOptions() // use embedded codemirror for code blocks options.codeEditor = prefs_.visualMarkdownCodeEditor().getValue(); - - // enable rmdImagePreview if we are an executable rmd - options.rmdImagePreview = target_.canExecuteChunks(); - + // highlight rmd example chunks options.rmdExampleHighlight = true; diff --git a/src/gwt/src/org/rstudio/studio/client/workbench/views/source/editors/text/visualmode/VisualModePanmirrorFormat.java b/src/gwt/src/org/rstudio/studio/client/workbench/views/source/editors/text/visualmode/VisualModePanmirrorFormat.java index 7eb0cfe24dd..266b13df054 100644 --- a/src/gwt/src/org/rstudio/studio/client/workbench/views/source/editors/text/visualmode/VisualModePanmirrorFormat.java +++ b/src/gwt/src/org/rstudio/studio/client/workbench/views/source/editors/text/visualmode/VisualModePanmirrorFormat.java @@ -255,6 +255,11 @@ private boolean isHugodownDocument() return getOutputFormats().contains("hugodown::md_document"); } + private boolean isGitHubDocument() + { + return getOutputFormats().contains("github_document"); + } + private boolean isDistillDocument() { return (sessionInfo_.getIsDistillProject() && isDocInProject()) || @@ -363,7 +368,11 @@ else if (isHugodownDocument()) { return new Pair("goldmark", ""); } - + // github document + else if (isGitHubDocument()) + { + return new Pair("gfm", ""); + } } return null; diff --git a/src/gwt/src/org/rstudio/studio/client/workbench/views/source/model/DocTabDragParams.java b/src/gwt/src/org/rstudio/studio/client/workbench/views/source/model/DocTabDragParams.java index 514bfab466b..fde5a146fde 100644 --- a/src/gwt/src/org/rstudio/studio/client/workbench/views/source/model/DocTabDragParams.java +++ b/src/gwt/src/org/rstudio/studio/client/workbench/views/source/model/DocTabDragParams.java @@ -51,7 +51,7 @@ public final native static DocTabDragParams create(String docId, tab_width : 0, cursor_offset : 0, source_position: position, - display_name : display_name + display_name : displayName } }-*/;