Category : C Source Code
Archive   : DTE50.ZIP
Filename : FINDREP.C

 
Output of file : FINDREP.C contained in archive : DTE50.ZIP
/*
* Written by Douglas Thomson (1989/1990)
*
* This source code is released into the public domain.
*/

/*
* Name: dte - Doug's Text Editor program - find/replace module
* Purpose: This file contains the functions relating to finding text
* and replacing text.
* It also contains the code for moving the cursor to various
* other positions in the file.
* File: findrep.c
* Author: Douglas Thomson
* System: this file is intended to be system-independent
* Date: October 1, 1989
*/

#ifdef HPXL
#include "commonh"
#include "findreph"
#include "utilsh"
#else
#include "common.h"
#include "findrep.h"
#include "utils.h"
#endif

/*
* prototypes for all functions in this file
*/
int set_flags ARGS((char *flag_str, int *flags, int *count));
int get_flags ARGS((int lines));
int mystrcmp ARGS((char *s1, char *s2));
int mystrcmpi ARGS((char *s1, char *s2));
void on_screen ARGS((windows *window, text_ptr cursor, int last));
void do_replace ARGS((windows *window, text_ptr start));
void do_last ARGS((windows *window));
void find_string ARGS((windows *window));
void replace_string ARGS((windows *window));
void goto_prep ARGS((windows *window));
void goto_complete ARGS((windows *window, text_ptr cursor));
void goto_marker ARGS((windows *window, int n));
void goto_top_file ARGS((windows *window));
void goto_end_file ARGS((windows *window));
text_ptr scan_forward ARGS((text_ptr start, char *opp, char *target));
text_ptr scan_backward ARGS((text_ptr start, char *opp, char *target));
void match_pair ARGS((windows *window, int forward));
void goto_line ARGS((windows *window));

/*
* find and replace flags
*/
#define F_BACKWARD 0x01 /* search backwards through file */
#define F_GLOBAL 0x02 /* search entire file */
#define F_LOCAL 0x04 /* search only marked block */
#define F_AUTO 0x08 /* behave as if user always answered yes */
#define F_IGNORE 0x10 /* ignore case differences */
#define F_WORD 0x20 /* match whole words only */
#define F_MATCH 0x40 /* match the case of the text being replaced */

/*
* Name: set_flags
* Purpose: To set up find and replace flags.
* Date: October 1, 1989
* Passed: flag_str: the flags chosen by the user
* Returns: flags: the equivalent binary flags
* count: the repeat count embedded in the flags
* TRUE if flags were OK, FALSE otherwise
*/
int set_flags(flag_str, flags, count)
char *flag_str;
int *flags;
int *count;
{
char *p; /* used to scan through flag_str for flags */

/*
* start with no flags set, then add those chosen
*/
*flags = 0;

/*
* assume just a single find / replace required
*/
*count = 1;

/*
* scan flag_str to work out which flags were chosen
*/
for (p=flag_str; *p; ++p) {
if (isdigit(*p)) {
/*
* extract an embedded repeat count
*/
*count = atoi(p);
while (isdigit(*++p)) {
;
}
--p;
if (*count <= 0) {
error(WARNING, "bad repeat count: %d", *count);
*count = 1;
return FALSE;
}
}
else if ((*p = toupper(*p)) == 'B') {
*flags |= F_BACKWARD;
}
else if (*p == 'G') {
*flags |= F_GLOBAL;
*count = GLOB_COUNT;
}
else if (*p == 'L') {
*flags |= F_LOCAL;
*count = GLOB_COUNT;
}
else if (*p == 'N') {
*flags |= F_AUTO;
}
else if (*p == 'U') {
*flags |= F_IGNORE;
}
else if (*p == 'W') {
*flags |= F_WORD;
}
else if (*p == 'M') {
*flags |= F_MATCH;
}
else {
error(WARNING, "unknown flag: %c", *p);
return FALSE;
}
}
return TRUE;
}

/*
* Name: get_flags
* Purpose: To input find and replace flags.
* Date: October 1, 1989
* Passed: lines: no. of lines up from bottom of screen
* Returns: [g_status.flags]: binary flags set
* [g_status.flag_str]: flags as character string
* [g_status.search_count]: repeat find count
* OK if flags were entered, ERROR if user wanted to abort
*/
int get_flags(lines)
int lines;
{
char flag_str[MAX_COLS]; /* temporary copy of g_status.flag_str */
int flags; /* temporary copy of g_status.flags */
int count; /* temporary copy of g_status.count */

/*
* use the previous flags as the default
*/
strcpy(flag_str, g_status.flag_str);

/*
* keep on asking for flags until something acceptable is entered
*/
for (;;) {
if (get_name("Options (B,G,L,M,N,n,U,W): ", lines, flag_str) !=
OK) {
return ERROR;
}
if (set_flags(flag_str, &flags, &count)) {

break;
}
}

g_status.flags = flags;
g_status.search_count = count;
strcpy(g_status.flag_str, flag_str);
return OK;
}

/*
* Name: mystrcmp
* Purpose: To compare two strings up to the length of the first.
* Date: October 1, 1989
* Passed: s1: first string to compare
* s2: second string to compare
* Returns: 0 if strings match
* <0 if s1 < s2
* >0 if s1 > s2
*/
int mystrcmp(s1, s2)
char *s1;
char *s2;
{
for(;;) {
if (*s1 == '\0') {
return 0;
}
if (*s1 != *s2) {
return *s1 - *s2;
}
++s1;
++s2;
}
}

/*
* Name: mystrcmpi
* Purpose: To compare two strings up to the length of the first, ignoring
* case.
* Date: October 1, 1989
* Passed: s1: first string to compare
* s2: second string to compare
* Returns: 0 if strings match
* <0 if s1 < s2
* >0 if s1 > s2
*/
int mystrcmpi(s1, s2)
char *s1;
char *s2;
{
for(;;) {
if (*s1 == '\0') {
return 0;
}
if (tolower(*s1) != tolower(*s2)) {
return tolower(*s1) - tolower(*s2);
}
++s1;
++s2;
}
}

/*
* Name: on_screen
* Purpose: To move the cursor to a new position, without redrawing the
* window unless the new position is off the window.
* Date: October 1, 1989
* Passed: window: information allowing access to current window etc
* cursor: the new target position in the text
* last: the last line considered to be on the screen (this
* only affects the bottom window)
*/
void on_screen(window, cursor, last)
windows *window;
text_ptr cursor;
int last;
{
text_ptr p; /* used to scan from current cursor towards new one */
int line; /* used to count screen line */

/*
* if this is the bottom window displayed, then some lines at the
* bottom may need to be reserved for messages.
* Otherwise, the last line available is simply the bottom line used
* for the window.
*/
last = min(g_display.nlines-last-1, window->bottom_line);

line = window->cline;
if (window->cursor >= cursor) {
/*
* new cursor position is above old one
*/
for (p=window->cursor; ; p--) {
if (*p == '\n') {
--line;
}
if (line < window->top_line) {
/*
* off top of screen, so place in middle of display
*/
window->cline = window->place_line;

/*
* now check that we are not wasting the top part of the
* screen
*/
line = window->cline;
for (p=cursor; ; p--) {
if (*p == '\n') {
--line;
}
if (*p == '\0') {
window->cline -= line - window->top_line;
break;
}
if (line < window->top_line) {
break;
}
}
break;
}
if (p <= cursor) {
/*
* position found on screen
*/
window->cline = line;
break;
}
}
}
else {
/*
* new cursor position is below current one
*/
for (p=window->cursor; ; p++) {
if (*p == '\n') {
++line;
}
if (line > last) {
/*
* off bottom of screen or window
*/
window->cline = window->place_line;

/*
* now check that we are not wasting the bottom part of the
* screen
*/
line = window->cline;
for (p=cursor; ; p++) {
if (*p == '\n') {
++line;
}
if (*p == '\0') {
window->cline += last - line;
break;
}
if (line > last) {
break;
}
}
break;
}
if (p >= cursor) {
window->cline = line;
break;
}
}
}
window->cursor = cursor;
}

/*
* Name: do_replace
* Purpose: To replace text once match has been found.
* Date: October 1, 1989
* Passed: window: information allowing access to current window etc
* start: location of start of matched text
*/
void do_replace(window, start)
windows *window;
text_ptr start;
{
int old_len; /* length of original text */
int new_len; /* length of replacement text */
text_ptr source; /* source of block move */
text_ptr dest; /* destination of block move */
long number; /* number of characters moved */
char new_text[MAX_COLS]; /* replacement text (case matched) */

old_len = strlen(g_status.pattern);
new_len = strlen(g_status.subst);

/*
* unless case is to be matched, the replacement text is exactly
* what the user entered
*/
strcpy(new_text, g_status.subst);

if (g_status.flags & F_MATCH) {
/*
* change case of new text to match old
*/
for (dest=new_text, source=start; *dest; ++dest) {
if (isupper(*source)) {
*dest = toupper(*dest);
}
else if (islower(*source)) {
*dest = tolower(*dest);
}

/*
* if the replacement is longer than the original text, then
* keep on matching the case of the last character of the
* original text
*/
if (++source >= start + old_len) {
--source;
}
}
}

/*
* move the text to either make room for the extra replacement text
* or to close up the gap left
*/
source = start + old_len;
dest = start + new_len;
number = g_status.end_mem - source;
hw_move(dest, source, number);

/*
* insert the replacement text
*/
for (dest=start, source=new_text; *source; ) {
*dest++ = *source++;
}

/*
* fix up any affected marks
*/
fix_marks(window, start, -(long)old_len);
fix_marks(window, start, (long) new_len);
}

/*
* Name: do_last
* Purpose: To repeat the previous find or replace operation.
* Date: October 1, 1989
* Passed: window: information allowing access to current window etc
* Notes: This function performs a very simple-minded and inefficient
* text matching algorithm. When I have more time, I might try
* replacing this with something like the Boyer-Moore string
* pattern matching algorithm...
*/
void do_last(window)
windows *window;
{
int len; /* length of current line */
int count; /* number of matches still to be made */
text_ptr start; /* start of area to be searched */
text_ptr end; /* end of area to be searched */
text_ptr orig; /* original cursor location in text */
text_ptr cursor; /* where cursor line would be if stopped now */
text_ptr final_cursor; /* where cursor should be placed at end */
char *pattern; /* pattern to be searched for */
int pat_len; /* length of pattern */
int rep_len; /* length of replacement text */
int result; /* find/replace this one? */
int backwards; /* searching backwards? */
cmp_func cmp; /* string comparison function in use */

un_copy_line(window);

if (strlen(g_status.pattern) == 0) {
error(WARNING, "nothing to search for");
return;
}

/*
* save current cursor position as previous
*/
len = linelen(window->cursor) - 1;
if (window->ccol < len) {
len = window->ccol;
}
orig = window->cursor + len;
window->file_info->marker[PREVIOUS] = orig;

/*
* work out where to start and end searching
*/
backwards = g_status.flags & F_BACKWARD;
pattern = g_status.pattern;
pat_len = strlen(pattern);
rep_len = strlen(g_status.subst);
if (g_status.flags & F_GLOBAL) {
if (backwards) {
end = window->file_info->start_text;
start = window->file_info->end_text-1 - pat_len;
cursor = start - prelinelen(start);
}
else {
cursor = start = window->file_info->start_text;
end = window->file_info->end_text-1 - pat_len;
}
}
else if (g_status.flags & F_LOCAL) {
if (!window->file_info->visible ||
window->file_info->marker[START_BLOCK] >=
window->file_info->marker[END_BLOCK]) {
error(WARNING, "no block to search");
return;
}
if (backwards) {
end = window->file_info->marker[START_BLOCK];
start = window->file_info->marker[END_BLOCK] - pat_len;
cursor = window->file_info->marker[END_BLOCK] -
prelinelen(window->file_info->marker[END_BLOCK]);
}
else {
start = window->file_info->marker[START_BLOCK];
end = window->file_info->marker[END_BLOCK] - pat_len;
cursor = window->file_info->marker[START_BLOCK] -
prelinelen(window->file_info->marker[START_BLOCK]);
}
}
else {
if (backwards) {
end = window->file_info->start_text;
start = orig - pat_len;
}
else {
start = orig + 1;
end = window->file_info->end_text-1 - pat_len;
}
cursor = window->cursor;
}

/*
* work out how to compare
*/
if (g_status.flags & (F_IGNORE | F_MATCH)) {
cmp = mystrcmpi;
}
else {
cmp = mystrcmp;
}

/*
* find the required number of matches
*/
count = g_status.search_count;
while (count > 0) {
/*
* check if finished searching
*/
if (backwards) {
if (start < end) {
break;
}
}
else {
if (start > end) {
break;
}
}

/*
* keep track of where cursor line may start
*/
if (*start == '\n') {
cursor = start+1;
}

/*
* try for match
*/
if ((*cmp)(pattern, (char *)start) == 0) {
if (!(g_status.flags & F_WORD) ||
!(myisalnum(*(start-1)) || myisalnum(*(start+pat_len)))
) {
/*
* we have a valid match
*/
--count;

/*
* position the cursor to show the user
*/
if (backwards) {
/*
* since we have not got to the \n yet, we must
* scan backwards until we find it, so that we
* can display the screen properly.
*/
cursor = start - prelinelen(start);
}
window->ccol = (int) (start - cursor);
if (window->ccol >= g_display.ncols) {
window->ccol = g_display.ncols-1;
}

/*
* since this might be the last match, remember the
* spot so that the cursor can be left there at the
* end
*/
final_cursor = cursor;

if (g_status.flags & F_AUTO) {
if (g_status.replace) {
/*
* replace the string
*/
do_replace(window, start);
if (!backwards) {
/*
* replace may have changed the position of the
* end of the search
*/
end += rep_len - pat_len;

/*
* don't risk recursive replace!
*/
start += rep_len - 1;
/*
* this fixes a problem with a replacement
* containing the pattern combined with
* multiple ^L replaces...
*/
window->ccol = (int) (start - cursor);
if (window->ccol >= g_display.ncols) {
window->ccol = g_display.ncols-1;
}
}
}
}
else {
/*
* see if it is on the current screen, and if so
* adjust cursor line accordingly; otherwise
* place in center of screen
*/
on_screen(window, cursor, 1);

/*
* if necessary, ask the user if this replacement
* should be made, or if this is the desired
* occurrence for find
*/
if (g_status.replace || count > 0) {
/*
* arrange for matched text to be highlighted
*/
g_status.match_start = start;
g_status.match_end = start + pat_len;

/*
* find out what to do
*/
set_prompt("this one? (y/n/a/q): ", 1);
result = display(get_ynaq, 1);

/*
* remove highlighting
*/
g_status.match_start = g_status.match_end;

/*
* do whatever is required
*/
switch (result) {
case A_ABORT:
case A_QUIT:
return;
case A_ALWAYS:
/*
* switch to automatic mode
*/
g_status.flags |= F_AUTO;
if (!g_status.replace) {
break;
}

/*
* if replacing, then fall through to
* replace this one before moving on to
* the rest of them
*/
case A_YES:
if (g_status.replace) {
/*
* replace the string
*/
do_replace(window, start);
if (!backwards) {
/*
* see comments above
*/
end += rep_len - pat_len;
start += rep_len - 1;
window->ccol = (int) (start - cursor);
if (window->ccol >= g_display.ncols) {
window->ccol = g_display.ncols-1;
}
}
}
else {
/*
* found desired occurrence
*/
return;
}
break;
case A_NO:
/*
* keep looking
*/
break;
}
}
}
}
}
/*
* try again starting one character nearer the end
*/
if (backwards) {
--start;
}
else {
++start;
}
}

/*
* report no matches if necessary
*/
if (count == g_status.search_count) {
error(WARNING, "no match");
}
else {
/*
* leave the cursor on the final match
*/
on_screen(window, final_cursor, 1);
}
}

/*
* Name: find_string
* Purpose: To set up and perform a find operation.
* Date: October 1, 1989
* Passed: window: information allowing access to current window etc
*/
void find_string(window)
windows *window;
{
char pattern[MAX_COLS]; /* text to be found */

/*
* get replacement text, using previous as default
*/
strcpy(pattern, g_status.pattern);
if (get_name("String to find: ", 1, pattern) != OK) {
return;
}
strcpy(g_status.pattern, pattern);

/*
* get find options to use
*/
if (get_flags(2) != OK) {
return;
}

/*
* record that this is a find operation
*/
g_status.replace = FALSE;

/*
* pretend we are repeating the previous find
*/
do_last(window);
}

/*
* Name: replace_string
* Purpose: To set up and perform a replace operation.
* Date: October 1, 1989
* Passed: window: information allowing access to current window etc
*/
void replace_string(window)
windows *window;
{
char pattern[MAX_COLS]; /* the old and replacement text */

/*
* get the old text, using the previous as the default
*/
strcpy(pattern, g_status.pattern);
if (get_name("String to find: ", 1, pattern) != OK) {
return;
}
strcpy(g_status.pattern, pattern);

/*
* get the replacement text, using the previous as the default
*/
strcpy(pattern, g_status.subst);
if (get_name("Replacement: ", 2, pattern) != OK) {
return;
}
strcpy(g_status.subst, pattern);

/*
* get the replace flags
*/
if (get_flags(3) != OK) {
return;
}

/*
* record that this is a replace operation
*/
g_status.replace = TRUE;

/*
* go away and do the replace
*/
do_last(window);
}

/*
* Name: goto_prep
* Purpose: To get ready to perform a goto operation, mainly by recording
* the current position as previous.
* Date: October 1, 1989
* Passed: window: information allowing access to current window etc
*/
void goto_prep(window)
windows *window;
{
int len; /* length of cursor line */

/*
* make sure there is no confusion with the line buffer
*/
un_copy_line(window);

/*
* set the previous marker to the cursor position
*/
len = linelen(window->cursor);
if (window->ccol < len) {
len = window->ccol;
}
window->file_info->marker[PREVIOUS] = window->cursor + len;
}

/*
* Name: goto_complete
* Purpose: To clean up after a goto operation, mainly by making sure the
* cursor is nicely positioned on the screen.
* Date: October 1, 1989
* Passed: window: information allowing access to current window etc
* cursor: final destination in file buffer
*/
void goto_complete(window, cursor)
windows *window;
text_ptr cursor;
{
on_screen(window, cursor - prelinelen(cursor), 0);
window->ccol = prelinelen(cursor);
if (window->ccol >= g_display.ncols) {
window->ccol = g_display.ncols-1;
}
}

/*
* Name: goto_marker
* Purpose: To move the cursor to a particular position marker.
* Date: October 1, 1989
* Passed: window: information allowing access to current window etc
* n: the position marker to be used
* Notes: n must be in the range 0 .. 9
*/
void goto_marker(window, n)
windows *window;
int n;
{
text_ptr cursor; /* desired cursor position */

un_copy_line(window);
cursor = window->file_info->marker[n];
if (cursor) {
goto_prep(window);
goto_complete(window, cursor);
}
else {
error(WARNING, "no marker set");
}
}

/*
* Name: goto_top_file
* Purpose: To move the cursor to the top of the file.
* Date: October 1, 1989
* Passed: window: information allowing access to current window etc
*/
void goto_top_file(window)
windows *window;
{
goto_prep(window);
goto_complete(window, window->file_info->start_text);
}

/*
* Name: goto_end_file
* Purpose: To move the cursor to the end of the file.
* Date: October 1, 1989
* Passed: window: information allowing access to current window etc
*/
void goto_end_file(window)
windows *window;
{
if (window->file_info->end_text > window->file_info->start_text) {
goto_prep(window);
goto_complete(window, window->file_info->end_text-1); /* -1 for \0
at end of text */
}
}

/*
* Name: scan_forward
* Purpose: To find the corresponding occurrence of target, ignoring
* embedded pairs of opp and target, searching forwards.
* Date: October 1, 1989
* Passed: start: position of character to be paired
* opp: the opposite to target, if any
* target: the string to be found
* Returns: the location of the corresponding target in the text buffer
*/
text_ptr scan_forward(start, opp, target)
text_ptr start;
char *opp;
char *target;
{
int count = 0; /* number of unmatched opposites found */

while (*++start) {
if (opp && mystrcmpi(opp, (char *) start) == 0) {
count++;
}
else if (mystrcmpi(target, (char *) start) == 0) {
if (count == 0) {
break;
}
--count;
}
}
return start;
}

/*
* Name: scan_backward
* Purpose: To find the corresponding occurrence of target, ignoring
* embedded pairs of opp and target, searching backwards.
* Date: October 1, 1989
* Passed: start: position of character to be paired
* opp: the opposite to target, if any
* target: the string to be found
* Returns: the location of the corresponding target in the text buffer
*/
text_ptr scan_backward(start, opp, target)
text_ptr start;
char *opp;
char *target;
{
int count = 0; /* number of unmatched opposites found */

while (*--start) {
if (opp && mystrcmpi(opp, (char *) start) == 0) {
count++;
}
else if (mystrcmpi(target, (char *) start) == 0) {
if (count == 0) {
break;
}
--count;
}
}
return start;
}

/*
* Name: match_pair
* Purpose: To find the corresponding pair to the character under the
* cursor.
* Date: October 1, 1989
* Passed: window: information allowing access to current window etc
* forward: the user would prefer to move forwards?
* Notes: If the cursor character differs from its pair, then the search
* direction is chosen automatically, regardless of what the
* user requested.
* The direction only affects single and double quotes.
* Searching is very simple-minded, and does not cope with things
* like brackets embedded within quoted strings.
*/
void match_pair(window, forward)
windows *window;
int forward;
{
text_ptr orig; /* cursor location in text */

/*
* make sure the character under the cursor is one that has a
* matched pair
*/
un_copy_line(window);
if (window->ccol >= linelen(window->cursor)) {
return;
}
orig = window->cursor + window->ccol;
if (strchr("[]{}()\"\'/\*bBeE", *orig) == NULL) {
return;
}
if (*orig == '/' && *(orig+1) != '*') {
return;
}
if (*orig == '*' && *(orig+1) != '/') {
return;
}
if (*orig == 'b' || *orig == 'B') {
if (mystrcmpi("begin", (char *) orig) != 0) {
return;
}
}
if (*orig == 'e' || *orig == 'E') {
if (mystrcmpi("end", (char *) orig) != 0) {
return;
}
}

/*
* record the cursor position as previous
*/
goto_prep(window);

/*
* find the matching pair
*/
switch (tolower(*orig)) {
case '[':
orig = scan_forward(orig, "[", "]");
break;
case '(':
orig = scan_forward(orig, "(", ")");
break;
case '{':
orig = scan_forward(orig, "{", "}");
break;
case 'b':
orig = scan_forward(orig, "begin", "end");
break;
case ']':
orig = scan_backward(orig, "]", "[");
break;
case ')':
orig = scan_backward(orig, ")", "(");
break;
case '}':
orig = scan_backward(orig, "}", "{");
break;
case 'e':
orig = scan_backward(orig, "end", "begin");
break;
case '"':
if (forward) {
orig = scan_forward(orig, NULL, "\"");
}
else {
orig = scan_backward(orig, NULL, "\"");
}
break;
case '\'':
if (forward) {
orig = scan_forward(orig, NULL, "\'");
}
else {
orig = scan_backward(orig, NULL, "\'");
}
break;
case '/':
orig = scan_forward(orig, NULL, "*\/");
break;
case '*':
orig = scan_backward(orig, NULL, "/\*");
break;
}

/*
* searching backward may leave us on the leading \0
*/
if (orig < window->file_info->start_text) {
orig = window->file_info->start_text;
}

/*
* now show the user what we have found
*/
goto_complete(window, orig);
}

/*
* Name: goto_line
* Purpose: To move the cursor to a particular line in the file
* Date: October 1, 1989
* Passed: window: information allowing access to current window etc
* Notes: Counting lines from the start of the file buffer is not
* very efficient...
*/
void goto_line(window)
windows *window;
{
int number; /* line number selected */
int i; /* lines passed so far */
char num_str[MAX_COLS]; /* line number as string */
text_ptr p; /* used to scan through file counting lines */

/*
* find out where we are going
*/
strcpy(num_str, "");
if (get_name("Line number: ", 1, num_str) != OK) {
return;
}
number = atoi(num_str);

/*
* start from the start of the file, and count lines until we
* get there.
*/
un_copy_line(window);
p = window->file_info->start_text;
for (i=1; i < number; i++) {
p = find_next(p);
if (p == NULL) {
error(WARNING, "only %d lines in file", i);
return;
}
}

/*
* found the line, now note the previous position and show the user.
*/
goto_prep(window);
goto_complete(window, p);
}


  3 Responses to “Category : C Source Code
Archive   : DTE50.ZIP
Filename : FINDREP.C

  1. Very nice! Thank you for this wonderful archive. I wonder why I found it only now. Long live the BBS file archives!

  2. This is so awesome! 😀 I’d be cool if you could download an entire archive of this at once, though.

  3. But one thing that puzzles me is the “mtswslnkmcjklsdlsbdmMICROSOFT” string. There is an article about it here. It is definitely worth a read: http://www.os2museum.com/wp/mtswslnk/